diff --git a/test/setup-test-node.js b/test/setup-test-node.js index be8d7d9d61..20c5bca861 100644 --- a/test/setup-test-node.js +++ b/test/setup-test-node.js @@ -4,21 +4,29 @@ const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); -const { Crypto } = require('../ts/context/Crypto'); -const { setEnvironment, Environment } = require('../ts/environment'); -const { HourCyclePreference } = require('../ts/types/I18N'); +const { Crypto } = require('../ts/context/Crypto.js'); +const { setEnvironment, Environment } = require('../ts/environment.js'); +const { HourCyclePreference } = require('../ts/types/I18N.js'); +const { default: package } = require('../ts/util/packageJson.js'); chai.use(chaiAsPromised); setEnvironment(Environment.Test, true); -const storageMap = new Map(); - // To replicate logic we have on the client side global.window = { Date, performance, SignalContext: { + getVersion: () => package.version, + config: { + serverUrl: 'https://127.0.0.1:9', + storageUrl: 'https://127.0.0.1:9', + updatesUrl: 'https://127.0.0.1:9', + resourcesUrl: 'https://127.0.0.1:9', + certificateAuthority: package.certificateAuthority, + version: package.version, + }, crypto: new Crypto(), getResolvedMessagesLocale: () => 'en', getResolvedMessagesLocaleDirection: () => 'ltr', @@ -27,11 +35,6 @@ global.window = { getLocaleOverride: () => null, }, i18n: key => `i18n(${key})`, - storage: { - get: key => storageMap.get(key), - put: async (key, value) => storageMap.set(key, value), - remove: async key => storageMap.clear(key), - }, }; // For ducks/network.getEmptyState() diff --git a/test/test.js b/test/test.js index 32cc50cdc8..3e11675eba 100644 --- a/test/test.js +++ b/test/test.js @@ -16,7 +16,6 @@ window.Events = { /* Delete the database before running any tests */ before(async () => { await window.testUtilities.initialize(); - await window.storage.fetch(); }); window.testUtilities.prepareTests(); diff --git a/ts/CI.ts b/ts/CI.ts index aa2fafcc20..89b4ff029c 100644 --- a/ts/CI.ts +++ b/ts/CI.ts @@ -18,6 +18,7 @@ import { isSignalRoute } from './util/signalRoutes.js'; import { strictAssert } from './util/assert.js'; import { MessageModel } from './models/messages.js'; import type { SocketStatuses } from './textsecure/SocketManager.js'; +import { itemStorage } from './textsecure/Storage.js'; const log = createLogger('CI'); @@ -217,7 +218,7 @@ export function getCI({ await AttachmentBackupManager.waitForIdle(); // Remove the disclaimer from conversation hero for screenshot backup test - await window.storage.put('isRestoredFromBackup', true); + await itemStorage.put('isRestoredFromBackup', true); } function unlink() { @@ -234,12 +235,9 @@ export function getCI({ async function resetReleaseNotesFetcher() { await Promise.all([ - window.textsecure.storage.put( - 'releaseNotesVersionWatermark', - '7.0.0-alpha.1' - ), - window.textsecure.storage.put('releaseNotesPreviousManifestHash', ''), - window.textsecure.storage.put('releaseNotesNextFetchTime', Date.now()), + itemStorage.put('releaseNotesVersionWatermark', '7.0.0-alpha.1'), + itemStorage.put('releaseNotesPreviousManifestHash', ''), + itemStorage.put('releaseNotesNextFetchTime', Date.now()), ]); } diff --git a/ts/CI/benchmarkConversationOpen.ts b/ts/CI/benchmarkConversationOpen.ts index 90da7bd92c..f56cf17ecc 100644 --- a/ts/CI/benchmarkConversationOpen.ts +++ b/ts/CI/benchmarkConversationOpen.ts @@ -17,6 +17,7 @@ import type { StatsType } from '../util/benchmark/stats.js'; import type { MessageAttributesType } from '../model-types.d.ts'; import { createLogger } from '../logging/log.js'; import { postSaveUpdates } from '../util/cleanup.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('benchmarkConversationOpen'); @@ -45,7 +46,7 @@ export async function populateConversationWithMessages({ const logId = 'benchmarkConversationOpen/populateConversationWithMessages'; log.info(`${logId}: populating conversation`); - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const conversation = window.ConversationController.get(conversationId); strictAssert( diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index e9c4fcb71c..0c5a0f0d82 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -8,7 +8,7 @@ import { v4 as generateUuid } from 'uuid'; import { DataReader, DataWriter } from './sql/Client.js'; import { createLogger } from './logging/log.js'; import * as Errors from './types/errors.js'; -import { getAuthorId } from './messages/helpers.js'; +import { getAuthorId } from './messages/sources.js'; import { maybeDeriveGroupV2Id } from './groups.js'; import { assertDev, strictAssert } from './util/assert.js'; import { drop } from './util/drop.js'; @@ -31,6 +31,8 @@ import { getServiceIdsForE164s } from './util/getServiceIdsForE164s.js'; import { SIGNAL_ACI, SIGNAL_AVATAR_PATH } from './types/SignalConversation.js'; import { getTitleNoDefault } from './util/getTitle.js'; import * as StorageService from './services/storage.js'; +import textsecureUtils from './textsecure/Helpers.js'; +import { cdsLookup } from './textsecure/WebAPI.js'; import type { ConversationPropsForUnreadStats } from './util/countUnreadStats.js'; import { countAllConversationsUnreadStats } from './util/countUnreadStats.js'; import { isTestOrMockEnvironment } from './environment.js'; @@ -54,6 +56,7 @@ import type { AciString, PniString, } from './types/ServiceId.js'; +import { itemStorage } from './textsecure/Storage.js'; const { debounce, pick, uniq, without } = lodash; @@ -360,7 +363,7 @@ export class ConversationController { } const includeMuted = - window.storage.get('badge-count-muted-conversations') || false; + itemStorage.get('badge-count-muted-conversations') || false; const unreadStats = countAllConversationsUnreadStats( this.#_conversations.map( @@ -383,7 +386,7 @@ export class ConversationController { { includeMuted } ); - drop(window.storage.put('unreadCount', unreadStats.unreadCount)); + drop(itemStorage.put('unreadCount', unreadStats.unreadCount)); if (unreadStats.unreadCount > 0) { const total = @@ -600,7 +603,7 @@ export class ConversationController { return null; } - const [id] = window.textsecure.utils.unencodeNumber(address); + const [id] = textsecureUtils.unencodeNumber(address); const conv = this.get(id); if (conv) { @@ -611,9 +614,9 @@ export class ConversationController { } getOurConversationId(): string | undefined { - const e164 = window.textsecure.storage.user.getNumber(); - const aci = window.textsecure.storage.user.getAci(); - const pni = window.textsecure.storage.user.getPni(); + const e164 = itemStorage.user.getNumber(); + const aci = itemStorage.user.getAci(); + const pni = itemStorage.user.getPni(); if (!e164 && !aci && !pni) { return undefined; @@ -685,7 +688,7 @@ export class ConversationController { } areWePrimaryDevice(): boolean { - const ourDeviceId = window.textsecure.storage.user.getDeviceId(); + const ourDeviceId = itemStorage.user.getDeviceId(); return ourDeviceId === 1; } @@ -1590,7 +1593,7 @@ export class ConversationController { } migrateAvatarsForNonAcceptedConversations(): void { - if (window.storage.get('avatarsHaveBeenMigrated')) { + if (itemStorage.get('avatarsHaveBeenMigrated')) { return; } const conversations = this.getAll(); @@ -1634,11 +1637,11 @@ export class ConversationController { log.info( `unset avatars for ${numberOfConversationsMigrated} unaccepted conversations` ); - drop(window.storage.put('avatarsHaveBeenMigrated', true)); + drop(itemStorage.put('avatarsHaveBeenMigrated', true)); } repairPinnedConversations(): void { - const pinnedIds = window.storage.get('pinnedConversationIds', []); + const pinnedIds = itemStorage.get('pinnedConversationIds', []); for (const id of pinnedIds) { const convo = this.get(id); @@ -1676,10 +1679,8 @@ export class ConversationController { // For testing async _forgetE164(e164: string): Promise { - const { server } = window.textsecure; - strictAssert(server, 'Server must be initialized'); const { entries: serviceIdMap, transformedE164s } = - await getServiceIdsForE164s(server, [e164]); + await getServiceIdsForE164s(cdsLookup, [e164]); const e164ToUse = transformedE164s.get(e164) ?? e164; const pni = serviceIdMap.get(e164ToUse)?.pni; diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index b20f939fdc..789d232206 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -3,7 +3,7 @@ import lodash from 'lodash'; -import type { WebAPIType } from './textsecure/WebAPI.js'; +import type { getConfig } from './textsecure/WebAPI.js'; import { createLogger } from './logging/log.js'; import type { AciString } from './types/ServiceId.js'; import { parseIntOrThrow } from './util/parseIntOrThrow.js'; @@ -13,6 +13,7 @@ import { uuidToBytes } from './util/uuidToBytes.js'; import { HashType } from './types/Crypto.js'; import { getCountryCode } from './types/PhoneNumber.js'; import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration.js'; +import type { StorageInterface } from './types/Storage.d.ts'; const { get, throttle } = lodash; @@ -69,8 +70,15 @@ type ConfigListenersMapType = { let config: ConfigMapType = {}; const listeners: ConfigListenersMapType = {}; -export function restoreRemoteConfigFromStorage(): void { - config = window.storage.get('remoteConfig') || {}; +export type OptionsType = Readonly<{ + getConfig: typeof getConfig; + storage: Pick; +}>; + +export function restoreRemoteConfigFromStorage({ + storage, +}: Pick): void { + config = storage.get('remoteConfig') || {}; } export function onChange( @@ -86,17 +94,18 @@ export function onChange( }; } -export const _refreshRemoteConfig = async ( - server: WebAPIType -): Promise => { +export const _refreshRemoteConfig = async ({ + getConfig, + storage, +}: OptionsType): Promise => { const now = Date.now(); - const oldConfigHash = window.storage.get('remoteConfigHash'); + const oldConfigHash = storage.get('remoteConfigHash'); const { config: newConfig, serverTimestamp, configHash, - } = await server.getConfig(oldConfigHash); + } = await getConfig(oldConfigHash); const serverTimeSkew = serverTimestamp - now; @@ -178,25 +187,25 @@ export const _refreshRemoteConfig = async ( const remoteExpirationValue = getValue('desktop.clientExpiration'); if (!remoteExpirationValue) { // If remote configuration fetch worked - we are not expired anymore. - if (window.storage.get('remoteBuildExpiration') != null) { + if (storage.get('remoteBuildExpiration') != null) { log.warn('Remote Config: clearing remote expiration on successful fetch'); } - await window.storage.remove('remoteBuildExpiration'); + await storage.remove('remoteBuildExpiration'); } else { const remoteBuildExpirationTimestamp = parseRemoteClientExpiration( remoteExpirationValue ); if (remoteBuildExpirationTimestamp) { - await window.storage.put( + await storage.put( 'remoteBuildExpiration', remoteBuildExpirationTimestamp ); } } - await window.storage.put('remoteConfig', config); - await window.storage.put('remoteConfigHash', configHash); - await window.storage.put('serverTimeSkew', serverTimeSkew); + await storage.put('remoteConfig', config); + await storage.put('remoteConfigHash', configHash); + await storage.put('serverTimeSkew', serverTimeSkew); }; export const maybeRefreshRemoteConfig = throttle( @@ -207,12 +216,12 @@ export const maybeRefreshRemoteConfig = throttle( ); export async function forceRefreshRemoteConfig( - server: WebAPIType, + options: OptionsType, reason: string ): Promise { log.info(`forceRefreshRemoteConfig: ${reason}`); maybeRefreshRemoteConfig.cancel(); - await _refreshRemoteConfig(server); + await _refreshRemoteConfig(options); } export function isEnabled( diff --git a/ts/SignalProtocolStore.ts b/ts/SignalProtocolStore.ts index dfdedd5b8b..89fc6053f3 100644 --- a/ts/SignalProtocolStore.ts +++ b/ts/SignalProtocolStore.ts @@ -68,6 +68,7 @@ import { } from './textsecure/AccountManager.js'; import { formatGroups, groupWhile } from './util/groupWhile.js'; import { parseUnknown } from './util/schemas.js'; +import { itemStorage } from './textsecure/Storage.js'; const { omit } = lodash; @@ -1811,7 +1812,7 @@ export class SignalProtocolStore extends EventEmitter { async lightSessionReset(qualifiedAddress: QualifiedAddress): Promise { const id = qualifiedAddress.toString(); - const sessionResets = window.storage.get( + const sessionResets = itemStorage.get( 'sessionResets', {} as SessionResetsType ); @@ -1827,7 +1828,7 @@ export class SignalProtocolStore extends EventEmitter { } sessionResets[id] = Date.now(); - await window.storage.put('sessionResets', sessionResets); + await itemStorage.put('sessionResets', sessionResets); try { const { serviceId } = qualifiedAddress; @@ -1854,7 +1855,7 @@ export class SignalProtocolStore extends EventEmitter { // If we failed to queue the session reset, then we'll allow another attempt sooner // than one hour from now. delete sessionResets[id]; - await window.storage.put('sessionResets', sessionResets); + await itemStorage.put('sessionResets', sessionResets); log.error( `lightSessionReset/${id}: Encountered error`, @@ -1945,7 +1946,7 @@ export class SignalProtocolStore extends EventEmitter { if (encodedAddress == null) { throw new Error('isTrustedIdentity: encodedAddress was undefined/null'); } - const isOurIdentifier = window.textsecure.storage.user.isOurServiceId( + const isOurIdentifier = itemStorage.user.isOurServiceId( encodedAddress.serviceId ); @@ -2143,7 +2144,7 @@ export class SignalProtocolStore extends EventEmitter { ); if (identityKeyChanged) { - const isOurIdentifier = window.textsecure.storage.user.isOurServiceId( + const isOurIdentifier = itemStorage.user.isOurServiceId( encodedAddress.serviceId ); @@ -2247,8 +2248,7 @@ export class SignalProtocolStore extends EventEmitter { const id = serviceId; // When saving a PNI identity - don't create a separate conversation - const serviceIdKind = - window.textsecure.storage.user.getOurServiceIdKind(serviceId); + const serviceIdKind = itemStorage.user.getOurServiceIdKind(serviceId); if (serviceIdKind !== ServiceIdKind.PNI) { window.ConversationController.getOrCreate(id, 'private'); } @@ -2565,8 +2565,6 @@ export class SignalProtocolStore extends EventEmitter { } async removeOurOldPni(oldPni: PniString): Promise { - const { storage } = window; - log.info(`removeOurOldPni(${oldPni})`); // Update caches @@ -2598,13 +2596,13 @@ export class SignalProtocolStore extends EventEmitter { // Update database await Promise.all([ - storage.put( + itemStorage.put( 'identityKeyMap', - omit(storage.get('identityKeyMap') || {}, oldPni) + omit(itemStorage.get('identityKeyMap') || {}, oldPni) ), - storage.put( + itemStorage.put( 'registrationIdMap', - omit(storage.get('registrationIdMap') || {}, oldPni) + omit(itemStorage.get('registrationIdMap') || {}, oldPni) ), DataWriter.removePreKeysByServiceId(oldPni), DataWriter.removeSignedPreKeysByServiceId(oldPni), @@ -2630,8 +2628,6 @@ export class SignalProtocolStore extends EventEmitter { ? KyberPreKeyRecord.deserialize(lastResortKyberPreKeyBytes) : undefined; - const { storage } = window; - const pniPublicKey = identityKeyPair.publicKey.serialize(); const pniPrivateKey = identityKeyPair.privateKey.serialize(); @@ -2641,21 +2637,21 @@ export class SignalProtocolStore extends EventEmitter { // Update database await Promise.all([ - storage.put('identityKeyMap', { - ...(storage.get('identityKeyMap') || {}), + itemStorage.put('identityKeyMap', { + ...(itemStorage.get('identityKeyMap') || {}), [pni]: { pubKey: pniPublicKey, privKey: pniPrivateKey, }, }), - storage.put('registrationIdMap', { - ...(storage.get('registrationIdMap') || {}), + itemStorage.put('registrationIdMap', { + ...(itemStorage.get('registrationIdMap') || {}), [pni]: registrationId, }), (async () => { const newId = signedPreKey.id() + 1; log.warn(`${logId}: Updating next signed pre key id to ${newId}`); - await storage.put(SIGNED_PRE_KEY_ID_KEY[ServiceIdKind.PNI], newId); + await itemStorage.put(SIGNED_PRE_KEY_ID_KEY[ServiceIdKind.PNI], newId); })(), this.storeSignedPreKey( pni, @@ -2673,7 +2669,7 @@ export class SignalProtocolStore extends EventEmitter { } const newId = lastResortKyberPreKey.id() + 1; log.warn(`${logId}: Updating next kyber pre key id to ${newId}`); - await storage.put(KYBER_KEY_ID_KEY[ServiceIdKind.PNI], newId); + await itemStorage.put(KYBER_KEY_ID_KEY[ServiceIdKind.PNI], newId); })(), lastResortKyberPreKeyBytes && lastResortKyberPreKey ? this.storeKyberPreKeys(pni, [ @@ -2694,8 +2690,8 @@ export class SignalProtocolStore extends EventEmitter { await DataWriter.removeAll(); await this.hydrateCaches(); - window.storage.reset(); - await window.storage.fetch(); + itemStorage.reset(); + await itemStorage.fetch(); window.ConversationController.reset(); await window.ConversationController.load(); @@ -2718,13 +2714,13 @@ export class SignalProtocolStore extends EventEmitter { await this.hydrateCaches(); - window.storage.reset(); - await window.storage.fetch(); + itemStorage.reset(); + await itemStorage.fetch(); } signAlternateIdentity(): PniSignatureMessageType | undefined { - const ourAci = window.textsecure.storage.user.getCheckedAci(); - const ourPni = window.textsecure.storage.user.getPni(); + const ourAci = itemStorage.user.getCheckedAci(); + const ourPni = itemStorage.user.getPni(); if (!ourPni) { log.error('signAlternateIdentity: No local pni'); return undefined; diff --git a/ts/background.ts b/ts/background.ts index 71003445ec..46e1d74d0f 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -72,9 +72,14 @@ import { isPollReceiveEnabled, } from './types/Polls.js'; import type { ConversationModel } from './models/conversations.js'; -import { getAuthor, isIncoming } from './messages/helpers.js'; +import { isIncoming } from './messages/helpers.js'; +import { getAuthor } from './messages/sources.js'; import { migrateBatchOfMessages } from './messages/migrateMessageData.js'; -import { createBatcher } from './util/batcher.js'; +import { createBatcher, waitForAllBatchers } from './util/batcher.js'; +import { + flushAllWaitBatchers, + waitForAllWaitBatchers, +} from './util/waitBatcher.js'; import { initializeAllJobQueues, shutdownAllJobQueues, @@ -118,7 +123,33 @@ import type { ViewOnceOpenSyncEvent, ViewSyncEvent, } from './textsecure/messageReceiverEvents.js'; -import type { WebAPIType } from './textsecure/WebAPI.js'; +import { + cancelInflightRequests, + checkSockets, + connect as connectWebAPI, + getConfig, + getHasSubscription, + getReleaseNote, + getReleaseNoteHash, + getReleaseNoteImageAttachment, + getReleaseNotesManifest, + getReleaseNotesManifestHash, + getSenderCertificate, + getServerAlerts, + getSocketStatus, + isOnline, + logout, + onExpiration, + onNavigatorOffline, + onNavigatorOnline, + reconnect as reconnectWebAPI, + registerCapabilities as doRegisterCapabilities, + registerRequestHandler, + reportMessage, + sendChallengeResponse, + unregisterRequestHandler, +} from './textsecure/WebAPI.js'; +import AccountManager from './textsecure/AccountManager.js'; import * as KeyChangeListener from './textsecure/KeyChangeListener.js'; import { UpdateKeysListener } from './textsecure/UpdateKeysListener.js'; import { isGroup } from './util/whatTypeOfConversation.js'; @@ -175,8 +206,7 @@ import { ReactionSource } from './reactions/ReactionSource.js'; import { singleProtoJobQueue } from './jobs/singleProtoJobQueue.js'; import { conversationJobQueue } from './jobs/conversationJobQueue.js'; import { SeenStatus } from './MessageSeenStatus.js'; -import MessageSender from './textsecure/SendMessage.js'; -import type AccountManager from './textsecure/AccountManager.js'; +import { MessageSender } from './textsecure/SendMessage.js'; import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate.js'; import { flushAttachmentDownloadQueue } from './util/attachmentDownloadQueue.js'; import { initializeRedux } from './state/initializeRedux.js'; @@ -243,6 +273,7 @@ import { NavTab, SettingsPage, ProfileEditorPage } from './types/Nav.js'; import { initialize as initializeDonationService } from './services/donations.js'; import { MessageRequestResponseSource } from './types/MessageRequestResponseEvent.js'; import { JobCancelReason } from './jobs/types.js'; +import { itemStorage } from './textsecure/Storage.js'; const { isNumber, throttle } = lodash; @@ -253,7 +284,7 @@ export function isOverHourIntoPast(timestamp: number): boolean { } export async function cleanupSessionResets(): Promise { - const sessionResets = window.storage.get( + const sessionResets = itemStorage.get( 'sessionResets', {} as SessionResetsType ); @@ -266,7 +297,7 @@ export async function cleanupSessionResets(): Promise { } }); - await window.storage.put('sessionResets', sessionResets); + await itemStorage.put('sessionResets', sessionResets); } export async function startApp(): Promise { @@ -284,18 +315,17 @@ export async function startApp(): Promise { StartupQueue.initialize(); notificationService.initialize({ i18n: window.i18n, - storage: window.storage, + storage: itemStorage, }); await initializeMessageCounter(); // Initialize WebAPI as early as possible - let server: WebAPIType | undefined; let messageReceiver: MessageReceiver | undefined; let challengeHandler: ChallengeHandler | undefined; let routineProfileRefresher: RoutineProfileRefresher | undefined; - ourProfileKeyService.initialize(window.storage); + ourProfileKeyService.initialize(itemStorage); window.SignalContext.activeWindowService.registerForChange(isActive => { if (!isActive) { @@ -407,13 +437,7 @@ export async function startApp(): Promise { }); window.getSocketStatus = () => { - if (server === undefined) { - return { - authenticated: { status: SocketStatus.CLOSED }, - unauthenticated: { status: SocketStatus.CLOSED }, - }; - } - return server.getSocketStatus(); + return getSocketStatus(); }; let accountManager: AccountManager; @@ -421,16 +445,13 @@ export async function startApp(): Promise { if (accountManager) { return accountManager; } - if (!server) { - throw new Error('getAccountManager: server is not available!'); - } - accountManager = new window.textsecure.AccountManager(server); + accountManager = new AccountManager(); accountManager.addEventListener('startRegistration', () => { pauseProcessing('startRegistration'); // 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()); + drop(logout()); authSocketConnectCount = 0; backupReady.reject(new Error('startRegistration')); @@ -441,7 +462,7 @@ export async function startApp(): Promise { accountManager.addEventListener('endRegistration', () => { window.Whisper.events.emit('userChanged', false); - drop(window.storage.put('postRegistrationSyncsStatus', 'incomplete')); + drop(itemStorage.put('postRegistrationSyncsStatus', 'incomplete')); registrationCompleted?.resolve(); drop(Registration.markDone()); }); @@ -501,7 +522,7 @@ export async function startApp(): Promise { } // Set a flag to delete IndexedDB on next startup if it wasn't deleted just now. - // We need to use direct data calls, since window.storage isn't ready yet. + // We need to use direct data calls, since storage isn't ready yet. await DataWriter.createOrUpdateItem({ id: 'indexeddb-delete-needed', value: true, @@ -510,55 +531,36 @@ export async function startApp(): Promise { } // We need this 'first' check because we don't want to start the app up any other time - // than the first time. And window.storage.fetch() will cause onready() to fire. + // than the first time. And storage.fetch() will cause onready() to fire. let first = true; - window.storage.onready(async () => { + itemStorage.onready(async () => { if (!first) { return; } first = false; - restoreRemoteConfigFromStorage(); + restoreRemoteConfigFromStorage({ + storage: itemStorage, + }); window.Whisper.events.on('firstEnvelope', checkFirstEnvelope); const buildExpirationService = new BuildExpirationService(); - const { config } = window.SignalContext; - - const WebAPI = window.textsecure.WebAPI.initialize({ - chatServiceUrl: config.serverUrl, - storageUrl: config.storageUrl, - updatesUrl: config.updatesUrl, - resourcesUrl: config.resourcesUrl, - cdnUrlObject: { - 0: config.cdnUrl0, - 2: config.cdnUrl2, - 3: config.cdnUrl3, - }, - certificateAuthority: config.certificateAuthority, - contentProxyUrl: config.contentProxyUrl, - proxyUrl: config.proxyUrl, - version: config.version, - disableIPv6: config.disableIPv6, - stripePublishableKey: config.stripePublishableKey, - }); - - server = WebAPI.connect({ - ...window.textsecure.storage.user.getWebAPICredentials(), - hasBuildExpired: buildExpirationService.hasBuildExpired(), - hasStoriesDisabled: window.storage.get('hasStoriesDisabled', false), - }); + drop( + connectWebAPI({ + ...itemStorage.user.getWebAPICredentials(), + hasBuildExpired: buildExpirationService.hasBuildExpired(), + hasStoriesDisabled: itemStorage.get('hasStoriesDisabled', false), + }) + ); buildExpirationService.on('expired', () => { - drop(server?.onExpiration('build')); + drop(onExpiration('build')); }); - window.textsecure.server = server; - window.textsecure.messaging = new window.textsecure.MessageSender(server); - challengeHandler = new ChallengeHandler({ - storage: window.storage, + storage: itemStorage, startQueue(conversationId: string) { conversationJobQueue.resolveVerificationWaiter(conversationId); @@ -573,11 +575,7 @@ export async function startApp(): Promise { }, async sendChallengeResponse(data) { - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error('sendChallengeResponse: messaging is not available!'); - } - await messaging.sendChallengeResponse(data); + await sendChallengeResponse(data); }, onChallengeFailed() { @@ -611,7 +609,7 @@ export async function startApp(): Promise { log.info('Initializing MessageReceiver'); messageReceiver = new MessageReceiver({ - storage: window.storage, + storage: itemStorage, serverTrustRoots: window.getServerTrustRoots(), }); window.ConversationController.registerDelayBeforeUpdatingRedux(() => { @@ -770,22 +768,25 @@ export async function startApp(): Promise { queuedEventListener(onDeviceNameChangeSync) ); - if (!window.storage.get('defaultConversationColor')) { + if (!itemStorage.get('defaultConversationColor')) { drop( - window.storage.put( - 'defaultConversationColor', - DEFAULT_CONVERSATION_COLOR - ) + itemStorage.put('defaultConversationColor', DEFAULT_CONVERSATION_COLOR) ); } senderCertificateService.initialize({ - server, + server: { + isOnline, + getSenderCertificate, + }, events: window.Whisper.events, - storage: window.storage, + storage: itemStorage, }); - areWeASubscriberService.update(window.storage, server); + areWeASubscriberService.update(itemStorage, { + isOnline, + getHasSubscription, + }); void cleanupSessionResets(); @@ -805,20 +806,16 @@ export async function startApp(): Promise { const attachmentDownloadStopPromise = AttachmentDownloadManager.stop(); const attachmentBackupStopPromise = AttachmentBackupManager.stop(); - server?.cancelInflightRequests(JobCancelReason.Shutdown); + cancelInflightRequests(JobCancelReason.Shutdown); // Stop background processing idleDetector.stop(); // Stop processing incoming messages if (messageReceiver) { - strictAssert( - server !== undefined, - 'WebAPI should be initialized together with MessageReceiver' - ); log.info('shutdown: shutting down messageReceiver'); pauseProcessing('shutdown'); - await window.waitForAllBatchers(); + await waitForAllBatchers(); } log.info('shutdown: flushing conversations'); @@ -878,10 +875,7 @@ export async function startApp(): Promise { // A number of still-to-queue database queries might be waiting inside batchers. // We wait for these to empty first, and then shut down the data interface. - await Promise.all([ - window.waitForAllBatchers(), - window.waitForAllWaitBatchers(), - ]); + await Promise.all([waitForAllBatchers(), waitForAllWaitBatchers()]); log.info( 'shutdown: waiting for all attachment backups & downloads to finish' @@ -917,21 +911,21 @@ export async function startApp(): Promise { ); const currentVersion = window.getVersion(); - lastVersion = window.storage.get('version'); + lastVersion = itemStorage.get('version'); newVersion = !lastVersion || currentVersion !== lastVersion; - await window.storage.put('version', currentVersion); + await itemStorage.put('version', currentVersion); if (newVersion && lastVersion) { log.info( `New version detected: ${currentVersion}; previous: ${lastVersion}` ); - const remoteBuildExpiration = window.storage.get('remoteBuildExpiration'); + const remoteBuildExpiration = itemStorage.get('remoteBuildExpiration'); if (remoteBuildExpiration) { log.info( `Clearing remoteBuildExpiration. Previous value was ${remoteBuildExpiration}` ); - await window.storage.remove('remoteBuildExpiration'); + await itemStorage.remove('remoteBuildExpiration'); } if (window.isBeforeVersion(lastVersion, '6.45.0-alpha')) { @@ -952,8 +946,8 @@ export async function startApp(): Promise { if (window.isBeforeVersion(lastVersion, 'v1.29.2-beta.1')) { // Stickers flags await Promise.all([ - window.storage.put('showStickersIntroduction', true), - window.storage.put('showStickerPickerHint', true), + itemStorage.put('showStickersIntroduction', true), + itemStorage.put('showStickerPickerHint', true), ]); } @@ -976,12 +970,12 @@ export async function startApp(): Promise { } if (window.isBeforeVersion(lastVersion, 'v5.18.0')) { - await window.storage.remove('senderCertificate'); - await window.storage.remove('senderCertificateNoE164'); + await itemStorage.remove('senderCertificate'); + await itemStorage.remove('senderCertificateNoE164'); } if (window.isBeforeVersion(lastVersion, 'v5.19.0')) { - await window.storage.remove(GROUP_CREDENTIALS_KEY); + await itemStorage.remove(GROUP_CREDENTIALS_KEY); } if (window.isBeforeVersion(lastVersion, 'v5.37.0-alpha')) { @@ -994,12 +988,12 @@ export async function startApp(): Promise { } if (window.isBeforeVersion(lastVersion, 'v5.51.0-beta.2')) { - await window.storage.put('groupCredentials', []); + await itemStorage.put('groupCredentials', []); await DataWriter.removeAllProfileKeyCredentials(); } if (window.isBeforeVersion(lastVersion, 'v6.38.0-beta.1')) { - await window.storage.remove('hasCompletedSafetyNumberOnboarding'); + await itemStorage.remove('hasCompletedSafetyNumberOnboarding'); } // This one should always be last - it could restart the app @@ -1010,38 +1004,38 @@ export async function startApp(): Promise { } if (window.isBeforeVersion(lastVersion, 'v7.3.0-beta.1')) { - await window.storage.remove('lastHeartbeat'); - await window.storage.remove('lastStartup'); + await itemStorage.remove('lastHeartbeat'); + await itemStorage.remove('lastStartup'); } if (window.isBeforeVersion(lastVersion, 'v7.8.0-beta.1')) { - await window.storage.remove('sendEditWarningShown'); - await window.storage.remove('formattingWarningShown'); + await itemStorage.remove('sendEditWarningShown'); + await itemStorage.remove('formattingWarningShown'); } if (window.isBeforeVersion(lastVersion, 'v7.21.0-beta.1')) { - await window.storage.remove( + await itemStorage.remove( 'hasRegisterSupportForUnauthenticatedDelivery' ); } if (window.isBeforeVersion(lastVersion, 'v7.33.0-beta.1')) { - await window.storage.remove('masterKeyLastRequestTime'); + await itemStorage.remove('masterKeyLastRequestTime'); } if (window.isBeforeVersion(lastVersion, 'v7.43.0-beta.1')) { - await window.storage.remove('primarySendsSms'); + await itemStorage.remove('primarySendsSms'); } if (window.isBeforeVersion(lastVersion, 'v7.56.0-beta.1')) { - await window.storage.remove('backupMediaDownloadIdle'); + await itemStorage.remove('backupMediaDownloadIdle'); } if ( window.isBeforeVersion(lastVersion, 'v7.57.0') && - window.storage.get('needProfileMovedModal') === undefined + itemStorage.get('needProfileMovedModal') === undefined ) { - await window.storage.put('needProfileMovedModal', true); + await itemStorage.put('needProfileMovedModal', true); } if (window.isBeforeVersion(lastVersion, 'v7.75.0-beta.1')) { @@ -1061,8 +1055,8 @@ export async function startApp(): Promise { window.i18n ); - if (newVersion || window.storage.get('needOrphanedAttachmentCheck')) { - await window.storage.remove('needOrphanedAttachmentCheck'); + if (newVersion || itemStorage.get('needOrphanedAttachmentCheck')) { + await itemStorage.remove('needOrphanedAttachmentCheck'); await DataWriter.cleanupOrphanedAttachments(); } @@ -1126,7 +1120,7 @@ export async function startApp(): Promise { } }); - retryPlaceholders.start(window.storage); + retryPlaceholders.start(itemStorage); setInterval(async () => { const now = Date.now(); @@ -1236,20 +1230,19 @@ export async function startApp(): Promise { ); } }); - // end of window.storage.onready() callback + // end of storage.onready() callback log.info('Storage fetch'); - drop(window.storage.fetch()); + drop(itemStorage.fetch()); function pauseProcessing(reason: string) { - strictAssert(server != null, 'WebAPI not initialized'); strictAssert( messageReceiver != null, 'messageReceiver must be initialized' ); StorageService.disableStorageService(reason); - server.unregisterRequestHandler(messageReceiver); + unregisterRequestHandler(messageReceiver); messageReceiver.stopProcessing(); } @@ -1257,10 +1250,10 @@ export async function startApp(): Promise { initializeRedux(getParametersForRedux()); window.Whisper.events.on('userChanged', (reconnect = false) => { - const newDeviceId = window.textsecure.storage.user.getDeviceId(); - const newNumber = window.textsecure.storage.user.getNumber(); - const newACI = window.textsecure.storage.user.getAci(); - const newPNI = window.textsecure.storage.user.getPni(); + const newDeviceId = itemStorage.user.getDeviceId(); + const newNumber = itemStorage.user.getNumber(); + const newACI = itemStorage.user.getAci(); + const newPNI = itemStorage.user.getPni(); const ourConversation = window.ConversationController.getOurConversation(); @@ -1274,7 +1267,7 @@ export async function startApp(): Promise { ourNumber: newNumber, ourAci: newACI, ourPni: newPNI, - regionCode: window.storage.get('regionCode'), + regionCode: itemStorage.get('regionCode'), }); if (reconnect) { @@ -1315,14 +1308,14 @@ export async function startApp(): Promise { window.Whisper.events.on('powerMonitorSuspend', () => { log.info('powerMonitor: suspend'); - server?.cancelInflightRequests(JobCancelReason.PowerMonitorSuspend); + cancelInflightRequests(JobCancelReason.PowerMonitorSuspend); suspendTasksWithTimeout(); }); window.Whisper.events.on('powerMonitorResume', () => { log.info('powerMonitor: resume'); - server?.checkSockets(); - server?.cancelInflightRequests(JobCancelReason.PowerMonitorResume); + checkSockets(); + cancelInflightRequests(JobCancelReason.PowerMonitorResume); resumeTasksWithTimeout(); }); @@ -1334,17 +1327,12 @@ export async function startApp(): Promise { const enqueueReconnectToWebSocket = () => { reconnectToWebSocketQueue.add(async () => { - if (!server) { - log.info('reconnectToWebSocket: No server. Early return.'); - return; - } - if (remotelyExpired) { return; } log.info('reconnectToWebSocket starting...'); - await server.reconnect(); + await reconnectWebAPI(); }); }; @@ -1369,8 +1357,8 @@ export async function startApp(): Promise { } log.error('remote expiration detected, disabling reconnects'); - drop(window.storage.put('remoteBuildExpiration', Date.now())); - drop(server?.onExpiration('remote')); + drop(itemStorage.put('remoteBuildExpiration', Date.now())); + drop(onExpiration('remote')); remotelyExpired = true; }); @@ -1397,21 +1385,23 @@ export async function startApp(): Promise { async function start() { // Storage is ready because `start()` is called from `storage.onready()` - strictAssert(server !== undefined, 'start: server not initialized'); initializeAllJobQueues({ - server, + server: { + isOnline, + reportMessage, + }, }); strictAssert(challengeHandler, 'start: challengeHandler'); await challengeHandler.load(); - if (!window.storage.user.getNumber()) { + if (!itemStorage.user.getNumber()) { const ourConversation = window.ConversationController.getOurConversation(); const ourE164 = ourConversation?.get('e164'); if (ourE164) { log.warn('Restoring E164 from our conversation'); - await window.storage.user.setNumber(ourE164); + await itemStorage.user.setNumber(ourE164); } } @@ -1420,7 +1410,7 @@ export async function startApp(): Promise { window.ConversationController.repairPinnedConversations(); } - if (!window.storage.get('avatarsHaveBeenMigrated', false)) { + if (!itemStorage.get('avatarsHaveBeenMigrated', false)) { window.ConversationController.migrateAvatarsForNonAcceptedConversations(); } } @@ -1431,14 +1421,14 @@ export async function startApp(): Promise { initializeNotificationProfilesService(); log.info('Blocked uuids cleanup: starting...'); - const blockedUuids = window.storage.get(BLOCKED_UUIDS_ID, []); + const blockedUuids = itemStorage.get(BLOCKED_UUIDS_ID, []); const blockedAcis = blockedUuids.filter(isAciString); const diff = blockedUuids.length - blockedAcis.length; if (diff > 0) { log.warn( `Blocked uuids cleanup: Found ${diff} non-ACIs in blocked list. Removing.` ); - await window.storage.put(BLOCKED_UUIDS_ID, blockedAcis); + await itemStorage.put(BLOCKED_UUIDS_ID, blockedAcis); } log.info('Blocked uuids cleanup: complete'); @@ -1448,7 +1438,7 @@ export async function startApp(): Promise { log.info( `Expiration start timestamp cleanup: Found ${messagesUnexpectedlyMissingExpirationStartTimestamp.length} messages for cleanup` ); - if (!window.textsecure.storage.user.getAci()) { + if (!itemStorage.user.getAci()) { log.info( "Expiration start timestamp cleanup: Canceling update; we don't have our own UUID" ); @@ -1479,7 +1469,7 @@ export async function startApp(): Promise { }); await DataWriter.saveMessages(newMessageAttributes, { - ourAci: window.textsecure.storage.user.getCheckedAci(), + ourAci: itemStorage.user.getCheckedAci(), postSaveUpdates, }); } @@ -1492,7 +1482,7 @@ export async function startApp(): Promise { const appContainer = document.getElementById('app-container'); strictAssert(appContainer != null, 'No #app-container'); createRoot(appContainer).render(createAppRoot(window.reduxStore)); - const hideMenuBar = window.storage.get('hide-menu-bar', false); + const hideMenuBar = itemStorage.get('hide-menu-bar', false); window.IPC.setAutoHideMenuBar(hideMenuBar); window.IPC.setMenuBarVisibility(!hideMenuBar); @@ -1510,13 +1500,13 @@ export async function startApp(): Promise { }); const isCoreDataValid = Boolean( - window.textsecure.storage.user.getAci() && + itemStorage.user.getAci() && window.ConversationController.getOurConversation() ); if (isCoreDataValid && Registration.everDone()) { idleDetector.start(); - if (window.storage.get('backupDownloadPath')) { + if (itemStorage.get('backupDownloadPath')) { window.reduxActions.installer.showBackupImport(); } else { window.reduxActions.app.openInbox(); @@ -1541,10 +1531,11 @@ export async function startApp(): Promise { // Maybe refresh remote configuration when we become active activeWindowService.registerForActive(async () => { - strictAssert(server !== undefined, 'WebAPI not ready'); - try { - await maybeRefreshRemoteConfig(server); + await maybeRefreshRemoteConfig({ + getConfig, + storage: itemStorage, + }); } catch (error) { if (error instanceof HTTPError) { log.warn( @@ -1564,7 +1555,7 @@ export async function startApp(): Promise { const remoteBuildExpirationTimestamp = parseRemoteClientExpiration(value); if (remoteBuildExpirationTimestamp) { drop( - window.storage.put( + itemStorage.put( 'remoteBuildExpiration', remoteBuildExpirationTimestamp ) @@ -1581,8 +1572,6 @@ export async function startApp(): Promise { } function setupNetworkChangeListeners() { - strictAssert(server, 'server must be initialized'); - const onOnline = () => { log.info('online'); drop(afterAuthSocketConnect()); @@ -1609,7 +1598,7 @@ export async function startApp(): Promise { if (messageReceiver) { drop(messageReceiver.drain()); - server?.unregisterRequestHandler(messageReceiver); + unregisterRequestHandler(messageReceiver); } if (hasAppEverBeenRegistered) { @@ -1636,9 +1625,9 @@ export async function startApp(): Promise { // Because these events may have already fired, we manually call their handlers. // isOnline() will return undefined if neither of these events have been emitted. - if (server.isOnline() === true) { + if (isOnline() === true) { onOnline(); - } else if (server.isOnline() === false) { + } else if (isOnline() === false) { onOffline(); } } @@ -1665,7 +1654,6 @@ export async function startApp(): Promise { return; } - strictAssert(server, 'server must be initialized'); strictAssert(messageReceiver, 'messageReceiver must be initialized'); while (afterAuthSocketConnectPromise?.promise) { @@ -1684,12 +1672,12 @@ export async function startApp(): Promise { await registrationCompleted?.promise; } - if (!window.textsecure.storage.user.getAci()) { + if (!itemStorage.user.getAci()) { log.error(`${logId}: ACI not captured during registration, unlinking`); return unlinkAndDisconnect(); } - if (!window.textsecure.storage.user.getPni()) { + if (!itemStorage.user.getPni()) { log.error(`${logId}: PNI not captured during registration, unlinking`); return unlinkAndDisconnect(); } @@ -1698,7 +1686,7 @@ export async function startApp(): Promise { if (isFirstAuthSocketConnect) { try { await forceRefreshRemoteConfig( - server, + { getConfig, storage: itemStorage }, 'afterAuthSocketConnect/firstConnect' ); } catch (error) { @@ -1712,7 +1700,7 @@ export async function startApp(): Promise { } const postRegistrationSyncsComplete = - window.storage.get('postRegistrationSyncsStatus') !== 'incomplete'; + itemStorage.get('postRegistrationSyncsStatus') !== 'incomplete'; // 3. Send any critical sync requests after registration if (!postRegistrationSyncsComplete) { @@ -1734,7 +1722,7 @@ export async function startApp(): Promise { // `messageReceiver.#isEmptied`. log.info(`${logId}: enabling message processing`); messageReceiver.startProcessingQueue(); - server.registerRequestHandler(messageReceiver); + registerRequestHandler(messageReceiver); // 6. Kickoff storage service sync if (isFirstAuthSocketConnect || !postRegistrationSyncsComplete) { @@ -1764,7 +1752,7 @@ export async function startApp(): Promise { try { log.info(`${logId}: waiting for postRegistrationSyncs`); await Promise.all(syncsToAwaitBeforeShowingInbox); - await window.storage.put('postRegistrationSyncsStatus', 'complete'); + await itemStorage.put('postRegistrationSyncsStatus', 'complete'); log.info(`${logId}: postRegistrationSyncs complete`); } catch (error) { log.error( @@ -1808,7 +1796,7 @@ export async function startApp(): Promise { async function maybeDownloadAndImportBackup(): Promise<{ wasBackupImported: boolean; }> { - const backupDownloadPath = window.storage.get('backupDownloadPath'); + const backupDownloadPath = itemStorage.get('backupDownloadPath'); const isLocalBackupAvailable = backupsService.isLocalBackupStaged() && isLocalBackupsEnabled(); @@ -1881,7 +1869,7 @@ export async function startApp(): Promise { const manager = window.getAccountManager(); await Promise.all([ manager.maybeUpdateDeviceName(), - window.textsecure.storage.user.removeSignalingKey(), + itemStorage.user.removeSignalingKey(), ]); } catch (e) { log.error( @@ -1893,14 +1881,13 @@ export async function startApp(): Promise { async function ensureAEP() { if ( - window.storage.get('accountEntropyPool') || + itemStorage.get('accountEntropyPool') || window.ConversationController.areWePrimaryDevice() ) { return; } - const lastSent = - window.storage.get('accountEntropyPoolLastRequestTime') ?? 0; + const lastSent = itemStorage.get('accountEntropyPoolLastRequestTime') ?? 0; const now = Date.now(); // If we last attempted sync one day in the past, or if we time @@ -1908,7 +1895,7 @@ export async function startApp(): Promise { 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); + await itemStorage.put('accountEntropyPoolLastRequestTime', now); } else { log.warn( 'ensureAEP: AEP not captured, but sync requested recently.' + @@ -1918,10 +1905,8 @@ export async function startApp(): Promise { } async function registerCapabilities() { - strictAssert(server, 'server must be initialized'); - try { - await server.registerCapabilities({ + await doRegisterCapabilities({ attachmentBackfill: true, spqr: true, }); @@ -1936,8 +1921,7 @@ export async function startApp(): Promise { function afterEveryAuthConnect() { log.info('afterAuthSocketConnect/afterEveryAuthConnect'); - strictAssert(server, 'afterEveryAuthConnect: server'); - drop(handleServerAlerts(server.getServerAlerts())); + drop(handleServerAlerts(getServerAlerts())); strictAssert(challengeHandler, 'afterEveryAuthConnect: challengeHandler'); drop(challengeHandler.onOnline()); @@ -1952,17 +1936,6 @@ export async function startApp(): Promise { } } - function onNavigatorOffline() { - log.info('navigator offline'); - - drop(server?.onNavigatorOffline()); - } - - function onNavigatorOnline() { - log.info('navigator online'); - drop(server?.onNavigatorOnline()); - } - window.addEventListener('online', onNavigatorOnline); window.addEventListener('offline', onNavigatorOffline); @@ -2080,12 +2053,7 @@ export async function startApp(): Promise { async function onEmpty({ isFromMessageReceiver, }: { isFromMessageReceiver?: boolean } = {}): Promise { - const { storage } = window.textsecure; - - await Promise.all([ - window.waitForAllBatchers(), - window.flushAllWaitBatchers(), - ]); + await Promise.all([waitForAllBatchers(), flushAllWaitBatchers()]); log.info('onEmpty: All outstanding database requests complete'); window.IPC.readyForUpdates(); window.ConversationController.onEmpty(); @@ -2118,7 +2086,7 @@ export async function startApp(): Promise { getAllConversations: () => window.ConversationController.getAll(), getOurConversationId: () => window.ConversationController.getOurConversationId(), - storage, + storage: itemStorage, }); void routineProfileRefresher.start(); @@ -2126,7 +2094,20 @@ export async function startApp(): Promise { drop(usernameIntegrity.start()); - drop(ReleaseNotesFetcher.init(window.Whisper.events, newVersion)); + drop( + ReleaseNotesFetcher.init( + { + isOnline, + getReleaseNote, + getReleaseNoteHash, + getReleaseNoteImageAttachment, + getReleaseNotesManifest, + getReleaseNotesManifestHash, + }, + window.Whisper.events, + newVersion + ) + ); drop(initializeDonationService()); @@ -2187,24 +2168,24 @@ export async function startApp(): Promise { linkPreviews, } = configuration; - await window.storage.put('read-receipt-setting', Boolean(readReceipts)); + await itemStorage.put('read-receipt-setting', Boolean(readReceipts)); if ( unidentifiedDeliveryIndicators === true || unidentifiedDeliveryIndicators === false ) { - await window.storage.put( + await itemStorage.put( 'unidentifiedDeliveryIndicators', unidentifiedDeliveryIndicators ); } if (typingIndicators === true || typingIndicators === false) { - await window.storage.put('typingIndicators', typingIndicators); + await itemStorage.put('typingIndicators', typingIndicators); } if (linkPreviews === true || linkPreviews === false) { - await window.storage.put('linkPreviews', linkPreviews); + await itemStorage.put('linkPreviews', linkPreviews); } } @@ -2215,7 +2196,7 @@ export async function startApp(): Promise { const { groupV2Id, started } = typing || {}; // We don't do anything with incoming typing messages if the setting is disabled - if (!window.storage.get('typingIndicators')) { + if (!itemStorage.get('typingIndicators')) { return; } @@ -2409,7 +2390,7 @@ export async function startApp(): Promise { }: EnvelopeUnsealedEvent): Promise { setInboxEnvelopeTimestamp(envelope.serverTimestamp); - const ourAci = window.textsecure.storage.user.getAci(); + const ourAci = itemStorage.user.getAci(); if ( envelope.sourceServiceId !== ourAci && isAciString(envelope.sourceServiceId) @@ -2464,7 +2445,7 @@ export async function startApp(): Promise { const sender = getAuthor(message.attributes); strictAssert(sender, 'MessageModel has no sender'); - const serviceIdKind = window.textsecure.storage.user.getOurServiceIdKind( + const serviceIdKind = itemStorage.user.getOurServiceIdKind( data.destinationServiceId ); @@ -2864,9 +2845,9 @@ export async function startApp(): Promise { sendStateByConversationId, sent_at: timestamp, serverTimestamp: data.serverTimestamp, - source: window.textsecure.storage.user.getNumber(), + source: itemStorage.user.getNumber(), sourceDevice: data.device, - sourceServiceId: window.textsecure.storage.user.getAci(), + sourceServiceId: itemStorage.user.getAci(), timestamp, type: data.message.isStory ? 'story' : 'outgoing', storyDistributionListId: data.storyDistributionListId, @@ -2954,8 +2935,8 @@ export async function startApp(): Promise { async function onSentMessage(event: SentEvent): Promise { const { data, confirm } = event; - const source = window.textsecure.storage.user.getNumber(); - const sourceServiceId = window.textsecure.storage.user.getAci(); + const source = itemStorage.user.getNumber(); + const sourceServiceId = itemStorage.user.getAci(); strictAssert(source && sourceServiceId, 'Missing user number and uuid'); // Make sure destination conversation is created before we hit getMessageDescriptor @@ -3158,7 +3139,7 @@ export async function startApp(): Promise { envelopeId: data.envelopeId, conversationId: message.attributes.conversationId, fromId: window.ConversationController.getOurConversationIdOrThrow(), - fromDevice: window.storage.user.getDeviceId() ?? 1, + fromDevice: itemStorage.user.getDeviceId() ?? 1, message: copyDataMessageIntoMessage(data.message, message.attributes), targetSentTimestamp: editedMessageTimestamp, removeFromMessageReceiverCache: confirm, @@ -3267,15 +3248,14 @@ export async function startApp(): Promise { if (messageReceiver) { log.info('unlinkAndDisconnect: logging out'); - strictAssert(server !== undefined, 'WebAPI not initialized'); pauseProcessing('unlinkAndDisconnect'); backupReady.reject(new Error('Aborted')); backupReady = explodePromise(); - await server.logout(); - await window.waitForAllBatchers(); + await logout(); + await waitForAllBatchers(); } void onEmpty({ isFromMessageReceiver: false }); @@ -3288,15 +3268,11 @@ export async function startApp(): Promise { const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex'; const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete'; - const previousNumberId = window.textsecure.storage.get(NUMBER_ID_KEY); - const previousUuidId = window.textsecure.storage.get(UUID_ID_KEY); - const previousPni = window.textsecure.storage.get(PNI_KEY); - const lastProcessedIndex = window.textsecure.storage.get( - LAST_PROCESSED_INDEX_KEY - ); - const isMigrationComplete = window.textsecure.storage.get( - IS_MIGRATION_COMPLETE_KEY - ); + const previousNumberId = itemStorage.get(NUMBER_ID_KEY); + const previousUuidId = itemStorage.get(UUID_ID_KEY); + const previousPni = itemStorage.get(PNI_KEY); + const lastProcessedIndex = itemStorage.get(LAST_PROCESSED_INDEX_KEY); + const isMigrationComplete = itemStorage.get(IS_MIGRATION_COMPLETE_KEY); try { log.info('unlinkAndDisconnect: removing configuration'); @@ -3321,32 +3297,29 @@ export async function startApp(): Promise { // These three bits of data are important to ensure that the app loads up // the conversation list, instead of showing just the QR code screen. if (previousNumberId !== undefined) { - await window.textsecure.storage.put(NUMBER_ID_KEY, previousNumberId); + await itemStorage.put(NUMBER_ID_KEY, previousNumberId); } if (previousUuidId !== undefined) { - await window.textsecure.storage.put(UUID_ID_KEY, previousUuidId); + await itemStorage.put(UUID_ID_KEY, previousUuidId); } if (previousPni !== undefined) { - await window.textsecure.storage.put(PNI_KEY, previousPni); + await itemStorage.put(PNI_KEY, previousPni); } // These two are important to ensure we don't rip through every message // in the database attempting to upgrade it after starting up again. - await window.textsecure.storage.put( + await itemStorage.put( IS_MIGRATION_COMPLETE_KEY, isMigrationComplete || false ); if (lastProcessedIndex !== undefined) { - await window.textsecure.storage.put( - LAST_PROCESSED_INDEX_KEY, - lastProcessedIndex - ); + await itemStorage.put(LAST_PROCESSED_INDEX_KEY, lastProcessedIndex); } else { - await window.textsecure.storage.remove(LAST_PROCESSED_INDEX_KEY); + await itemStorage.remove(LAST_PROCESSED_INDEX_KEY); } // Re-hydrate items from memory; removeAllConfiguration above changed database - await window.storage.fetch(); + await itemStorage.fetch(); log.info('unlinkAndDisconnect: Successfully cleared local configuration'); } catch (eraseError) { @@ -3405,8 +3378,8 @@ export async function startApp(): Promise { switch (eventType) { case FETCH_LATEST_ENUM.LOCAL_PROFILE: { log.info('onFetchLatestSync: fetching latest local profile'); - const ourAci = window.textsecure.storage.user.getAci() ?? null; - const ourE164 = window.textsecure.storage.user.getNumber() ?? null; + const ourAci = itemStorage.user.getAci() ?? null; + const ourE164 = itemStorage.user.getNumber() ?? null; await getProfile({ serviceId: ourAci, e164: ourE164, @@ -3420,8 +3393,10 @@ export async function startApp(): Promise { break; case FETCH_LATEST_ENUM.SUBSCRIPTION_STATUS: log.info('onFetchLatestSync: fetching latest subscription status'); - strictAssert(server, 'WebAPI not ready'); - areWeASubscriberService.update(window.storage, server); + areWeASubscriberService.update(itemStorage, { + isOnline, + getHasSubscription, + }); break; default: log.info(`onFetchLatestSync: Unknown type encountered ${eventType}`); @@ -3433,11 +3408,11 @@ export async function startApp(): Promise { async function onKeysSync(ev: KeysEvent) { const { accountEntropyPool, masterKey, mediaRootBackupKey } = ev; - const prevMasterKeyBase64 = window.storage.get('masterKey'); + const prevMasterKeyBase64 = itemStorage.get('masterKey'); const prevMasterKey = prevMasterKeyBase64 ? Bytes.fromBase64(prevMasterKeyBase64) : undefined; - const prevAccountEntropyPool = window.storage.get('accountEntropyPool'); + const prevAccountEntropyPool = itemStorage.get('accountEntropyPool'); let derivedMasterKey = masterKey; if (derivedMasterKey == null && accountEntropyPool) { @@ -3451,44 +3426,44 @@ export async function startApp(): Promise { if (prevAccountEntropyPool != null) { log.warn('onKeysSync: deleting window.accountEntropyPool'); } - await window.storage.remove('accountEntropyPool'); + await itemStorage.remove('accountEntropyPool'); } else { if (prevAccountEntropyPool !== accountEntropyPool) { log.info('onKeysSync: updating accountEntropyPool'); } - await window.storage.put('accountEntropyPool', accountEntropyPool); + await itemStorage.put('accountEntropyPool', accountEntropyPool); } if (derivedMasterKey == null) { if (prevMasterKey != null) { log.warn('onKeysSync: deleting window.masterKey'); } - await window.storage.remove('masterKey'); + await itemStorage.remove('masterKey'); } else { if (!Bytes.areEqual(derivedMasterKey, prevMasterKey)) { log.info('onKeysSync: updating masterKey'); } // Override provided storageServiceKey because it is deprecated. - await window.storage.put('masterKey', Bytes.toBase64(derivedMasterKey)); + await itemStorage.put('masterKey', Bytes.toBase64(derivedMasterKey)); } - const prevMediaRootBackupKey = window.storage.get('backupMediaRootKey'); + const prevMediaRootBackupKey = itemStorage.get('backupMediaRootKey'); if (mediaRootBackupKey == null) { if (prevMediaRootBackupKey != null) { log.warn('onKeysSync: deleting window.backupMediaRootKey'); } - await window.storage.remove('backupMediaRootKey'); + await itemStorage.remove('backupMediaRootKey'); } else { if (!Bytes.areEqual(prevMediaRootBackupKey, mediaRootBackupKey)) { log.info('onKeysSync: updating window.backupMediaRootKey'); } - await window.storage.put('backupMediaRootKey', mediaRootBackupKey); + await itemStorage.put('backupMediaRootKey', mediaRootBackupKey); } if (derivedMasterKey != null) { const storageServiceKey = deriveStorageServiceKey(derivedMasterKey); const storageServiceKeyBase64 = Bytes.toBase64(storageServiceKey); - if (window.storage.get('storageKey') === storageServiceKeyBase64) { + if (itemStorage.get('storageKey') === storageServiceKeyBase64) { log.info( "onKeysSync: storage service key didn't change, " + 'fetching manifest anyway' @@ -3498,7 +3473,7 @@ export async function startApp(): Promise { 'onKeysSync: updated storage service key, erasing state and fetching' ); try { - await window.storage.put('storageKey', storageServiceKeyBase64); + await itemStorage.put('storageKey', storageServiceKeyBase64); await StorageService.eraseAllStorageServiceState({ keepUnknownFields: true, }); @@ -3882,7 +3857,7 @@ export async function startApp(): Promise { const logId = `onDeleteForMeSync(${timestamp})`; // The user clearly knows about this feature; they did it on another device! - drop(window.storage.put('localDeleteWarningShown', true)); + drop(itemStorage.put('localDeleteWarningShown', true)); log.info(`${logId}: Saving ${deleteForMeSync.length} sync tasks`); diff --git a/ts/badges/badgeImageFileDownloader.ts b/ts/badges/badgeImageFileDownloader.ts index 31cd301fa9..d4cf004bb6 100644 --- a/ts/badges/badgeImageFileDownloader.ts +++ b/ts/badges/badgeImageFileDownloader.ts @@ -7,6 +7,7 @@ import { createLogger } from '../logging/log.js'; import { MINUTE } from '../util/durations/index.js'; import { missingCaseError } from '../util/missingCaseError.js'; import { waitForOnline } from '../util/waitForOnline.js'; +import { getBadgeImageFile, isOnline } from '../textsecure/WebAPI.js'; const log = createLogger('badgeImageFileDownloader'); @@ -88,16 +89,9 @@ function getUrlsToDownload(): Array { } async function downloadBadgeImageFile(url: string): Promise { - await waitForOnline({ timeout: 1 * MINUTE }); + await waitForOnline({ server: { isOnline }, timeout: 1 * MINUTE }); - const { server } = window.textsecure; - if (!server) { - throw new Error( - 'downloadBadgeImageFile: window.textsecure.server is not available!' - ); - } - - const imageFileData = await server.getBadgeImageFile(url); + const imageFileData = await getBadgeImageFile(url); const localPath = await window.Signal.Migrations.writeNewBadgeImageFileData(imageFileData); diff --git a/ts/challenge.ts b/ts/challenge.ts index a5b1b91351..c4168b7281 100644 --- a/ts/challenge.ts +++ b/ts/challenge.ts @@ -167,7 +167,7 @@ export class ChallengeHandler { // The initialization order is following: // - // 1. `.load()` when the `window.storage` is ready + // 1. `.load()` when the `storage` is ready // 2. `.onOnline()` when we connected to the server // // Wait for `.onOnline()` to trigger the retries instead of triggering diff --git a/ts/components/CallManager.stories.tsx b/ts/components/CallManager.stories.tsx index b408dac085..2001f82153 100644 --- a/ts/components/CallManager.stories.tsx +++ b/ts/components/CallManager.stories.tsx @@ -33,7 +33,6 @@ import { getDefaultCallLinkConversation, } from '../test-helpers/fakeCallLink.js'; import { allRemoteParticipants } from './CallScreen.stories.js'; -import { getPlaceholderContact } from '../state/selectors/conversations.js'; const { i18n } = window.SignalContext; @@ -51,13 +50,23 @@ const getConversation = () => lastUpdated: Date.now(), }); +const placeHolderContact: ConversationType = { + acceptedMessageRequest: false, + badges: [], + id: '123', + type: 'direct', + title: i18n('icu:unknownContact'), + isMe: false, + sharedGroupNames: [], +}; + const getUnknownContact = (): ConversationType => ({ - ...getPlaceholderContact(), + ...placeHolderContact, serviceId: generateAci(), }); const getUnknownParticipant = (): GroupCallRemoteParticipantType => ({ - ...getPlaceholderContact(), + ...placeHolderContact, serviceId: generateAci(), aci: generateAci(), demuxId: Math.round(10000 * Math.random()), @@ -105,6 +114,7 @@ const createProps = (storyProps: Partial = {}): PropsType => ({ hangUpActiveCall: action('hang-up-active-call'), hasInitialLoadCompleted: true, i18n, + isOnline: true, ringingCall: null, callLink: storyProps.callLink ?? undefined, me: { @@ -324,7 +334,7 @@ export function CallLinkLobbyParticipants1Unknown(): JSX.Element { VideoFrameSource; getIsSharingPhoneNumberWithEverybody: () => boolean; getPresentingSources: () => void; + isOnline: boolean; ringingCall: DirectIncomingCall | GroupIncomingCall | null; renderDeviceSelection: () => JSX.Element; renderReactionPicker: ( @@ -187,6 +188,7 @@ function ActiveCallManager({ denyUser, hangUpActiveCall, i18n, + isOnline, getIsSharingPhoneNumberWithEverybody, getGroupCallVideoFrameSource, getPresentingSources, @@ -386,6 +388,7 @@ function ActiveCallManager({ isAdhocJoinRequestPending={isAdhocJoinRequestPending} isCallFull={isCallFull} isConversationTooBigToRing={isConvoTooBigToRing} + isOnline={isOnline} getIsSharingPhoneNumberWithEverybody={ getIsSharingPhoneNumberWithEverybody } @@ -560,6 +563,7 @@ export function CallManager({ hangUpActiveCall, hasInitialLoadCompleted, i18n, + isOnline, getIsSharingPhoneNumberWithEverybody, me, notifyForCall, @@ -677,6 +681,7 @@ export function CallManager({ getPresentingSources={getPresentingSources} hangUpActiveCall={hangUpActiveCall} i18n={i18n} + isOnline={isOnline} getIsSharingPhoneNumberWithEverybody={ getIsSharingPhoneNumberWithEverybody } diff --git a/ts/components/CallingLobby.stories.tsx b/ts/components/CallingLobby.stories.tsx index 2800e15ada..765b2b261a 100644 --- a/ts/components/CallingLobby.stories.tsx +++ b/ts/components/CallingLobby.stories.tsx @@ -70,6 +70,7 @@ const createProps = (overrideProps: Partial = {}): PropsType => { isAdhocJoinRequestPending: overrideProps.isAdhocJoinRequestPending ?? false, isConversationTooBigToRing: false, isCallFull: overrideProps.isCallFull ?? false, + isOnline: true, getIsSharingPhoneNumberWithEverybody: overrideProps.getIsSharingPhoneNumberWithEverybody ?? (() => false), me: diff --git a/ts/components/CallingLobby.tsx b/ts/components/CallingLobby.tsx index 14e5bf661a..42f1aff599 100644 --- a/ts/components/CallingLobby.tsx +++ b/ts/components/CallingLobby.tsx @@ -21,7 +21,6 @@ import { import { CallMode } from '../types/CallDisposition.js'; import type { CallingConversationType } from '../types/Calling.js'; import type { LocalizerType } from '../types/Util.js'; -import { useIsOnline } from '../hooks/useIsOnline.js'; import * as KeyboardLayout from '../services/keyboardLayout.js'; import type { ConversationType } from '../state/ducks/conversations.js'; import { useCallingToasts } from './CallingToast.js'; @@ -65,6 +64,7 @@ export type PropsType = { isAdhocJoinRequestPending: boolean; isConversationTooBigToRing: boolean; isCallFull?: boolean; + isOnline: boolean; me: Readonly< Pick >; @@ -94,6 +94,7 @@ export function CallingLobby({ isAdhocJoinRequestPending, isCallFull = false, isConversationTooBigToRing, + isOnline, getIsSharingPhoneNumberWithEverybody, me, onCallCanceled, @@ -154,8 +155,6 @@ export function CallingLobby({ }; }, [toggleVideo, toggleAudio]); - const isOnline = useIsOnline(); - const [isCallConnecting, setIsCallConnecting] = React.useState( isAdhocJoinRequestPending || false ); diff --git a/ts/components/CallingPreCallInfo.stories.tsx b/ts/components/CallingPreCallInfo.stories.tsx index 00829f89b3..480ac47625 100644 --- a/ts/components/CallingPreCallInfo.stories.tsx +++ b/ts/components/CallingPreCallInfo.stories.tsx @@ -8,7 +8,6 @@ import { getDefaultConversation } from '../test-helpers/getDefaultConversation.j import type { PropsType } from './CallingPreCallInfo.js'; import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo.js'; import type { ConversationType } from '../state/ducks/conversations.js'; -import { getPlaceholderContact } from '../state/selectors/conversations.js'; import { generateAci } from '../types/ServiceId.js'; import { FAKE_CALL_LINK } from '../test-helpers/fakeCallLink.js'; import { callLinkToConversation } from '../util/callLinks.js'; @@ -27,7 +26,13 @@ const getDefaultGroupConversation = () => const otherMembers = times(6, () => getDefaultConversation()); const getUnknownContact = (): ConversationType => ({ - ...getPlaceholderContact(), + acceptedMessageRequest: false, + badges: [], + id: '123', + type: 'direct', + title: i18n('icu:unknownContact'), + isMe: false, + sharedGroupNames: [], serviceId: generateAci(), }); diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index d5720adfcd..71e9120dab 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -41,6 +41,7 @@ import type { PushPanelForConversationActionType, ShowConversationType, } from '../state/ducks/conversations.js'; +import type { GetConversationByIdType } from '../state/selectors/conversations.js'; import type { LinkPreviewForUIType } from '../types/message/LinkPreviews.js'; import { isSameLinkPreview } from '../types/message/LinkPreviews.js'; @@ -97,6 +98,7 @@ export type OwnProps = Readonly<{ bodyRanges: DraftBodyRanges | undefined ) => HydratedBodyRangesType | undefined; conversationId: string; + conversationSelector: GetConversationByIdType; discardEditMessage: (id: string) => unknown; draftEditMessage: DraftEditMessageType | null; draftAttachments: ReadonlyArray; @@ -250,6 +252,8 @@ export const CompositionArea = memo(function CompositionArea({ theme, setMuteExpiration, + // MediaEditor + conversationSelector, // AttachmentList draftAttachments, onClearAttachments, @@ -963,6 +967,7 @@ export const CompositionArea = memo(function CompositionArea({ isCreatingStory={false} isFormattingEnabled={isFormattingEnabled} isSending={false} + conversationSelector={conversationSelector} onClose={() => setAttachmentToEdit(undefined)} onDone={({ caption, diff --git a/ts/components/LeftPane.stories.tsx b/ts/components/LeftPane.stories.tsx index 17f699dbe1..a0b3c30bb3 100644 --- a/ts/components/LeftPane.stories.tsx +++ b/ts/components/LeftPane.stories.tsx @@ -33,7 +33,7 @@ import { useUuidFetchState, } from '../test-helpers/fakeLookupConversationWithoutServiceId.js'; import type { GroupListItemConversationType } from './conversationList/GroupListItem.js'; -import { ServerAlert } from '../util/handleServerAlerts.js'; +import { ServerAlert } from '../types/ServerAlert.js'; import { LeftPaneChatFolders } from './leftPane/LeftPaneChatFolders.js'; import { LeftPaneConversationListItemContextMenu } from './leftPane/LeftPaneConversationListItemContextMenu.js'; @@ -175,6 +175,17 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => { endConversationSearch: action('endConversationSearch'), endSearch: action('endSearch'), getPreferredBadge: () => undefined, + getServerAlertToShow: alerts => { + if (alerts[ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE]) { + return ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE; + } + + if (alerts[ServerAlert.IDLE_PRIMARY_DEVICE]) { + return ServerAlert.IDLE_PRIMARY_DEVICE; + } + + return null; + }, hasFailedStorySends: false, hasPendingUpdate: false, i18n, @@ -339,6 +350,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => { selectedConversationId: undefined, targetedMessageId: undefined, openUsernameReservationModal: action('openUsernameReservationModal'), + saveAlerts: async () => action('saveAlerts')(), savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'), searchInConversation: action('searchInConversation'), setComposeSearchTerm: action('setComposeSearchTerm'), diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 8caa541b55..f79657f2e6 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -56,7 +56,7 @@ import { import { ContextMenu } from './ContextMenu.js'; import type { UnreadStats } from '../util/countUnreadStats.js'; import { BackupMediaDownloadProgress } from './BackupMediaDownloadProgress.js'; -import type { ServerAlertsType } from '../util/handleServerAlerts.js'; +import type { ServerAlertsType, ServerAlert } from '../types/ServerAlert.js'; import { getServerAlertDialog } from './ServerAlerts.js'; import { NavTab, SettingsPage, ProfileEditorPage } from '../types/Nav.js'; import type { Location } from '../types/Nav.js'; @@ -118,6 +118,7 @@ export type PropsType = { mode: LeftPaneMode.SetGroupMetadata; } & LeftPaneSetGroupMetadataPropsType); getPreferredBadge: PreferredBadgeSelectorType; + getServerAlertToShow: (alerts: ServerAlertsType) => ServerAlert | null; i18n: LocalizerType; isMacOS: boolean; isNotificationProfileActive: boolean; @@ -152,6 +153,7 @@ export type PropsType = { onOutgoingAudioCallInConversation: (conversationId: string) => void; onOutgoingVideoCallInConversation: (conversationId: string) => void; removeConversation: (conversationId: string) => void; + saveAlerts: (alerts: ServerAlertsType) => Promise; savePreferredLeftPaneWidth: (_: number) => void; searchInConversation: (conversationId: string) => unknown; setComposeGroupAvatar: (_: undefined | Uint8Array) => void; @@ -225,6 +227,7 @@ export function LeftPane({ endConversationSearch, endSearch, getPreferredBadge, + getServerAlertToShow, hasExpiredDialog, hasFailedStorySends, hasNetworkDialog, @@ -260,6 +263,7 @@ export function LeftPane({ renderUpdateDialog, renderToastManager, resumeBackupMediaDownload, + saveAlerts, savePreferredLeftPaneWidth, searchInConversation, selectedConversationId, @@ -628,6 +632,8 @@ export function LeftPane({ const maybeServerAlert = getServerAlertDialog( serverAlerts, + getServerAlertToShow, + saveAlerts, commonDialogProps ); // Yellow dialogs diff --git a/ts/components/MediaEditor.tsx b/ts/components/MediaEditor.tsx index 7ccbdd5688..e43d9db9e5 100644 --- a/ts/components/MediaEditor.tsx +++ b/ts/components/MediaEditor.tsx @@ -12,7 +12,6 @@ import React, { import classNames from 'classnames'; import { createPortal } from 'react-dom'; import { fabric } from 'fabric'; -import { useSelector } from 'react-redux'; import lodash from 'lodash'; import type { DraftBodyRanges } from '../types/BodyRange.js'; import type { ImageStateType } from '../mediaEditor/ImageStateType.js'; @@ -48,7 +47,6 @@ import { ThemeType } from '../types/Util.js'; import { arrow } from '../util/keyboard.js'; import { canvasToBytes } from '../util/canvasToBytes.js'; import { loadImage } from '../util/loadImage.js'; -import { getConversationSelector } from '../state/selectors/conversations.js'; import { hydrateRanges } from '../types/BodyRange.js'; import { useConfirmDiscard } from '../hooks/useConfirmDiscard.js'; import { useFabricHistory } from '../mediaEditor/useFabricHistory.js'; @@ -64,6 +62,7 @@ import type { FunStickerSelection } from './fun/panels/FunPanelStickers.js'; import { drop } from '../util/drop.js'; import type { FunTimeStickerStyle } from './fun/constants.js'; import * as Errors from '../types/errors.js'; +import type { GetConversationByIdType } from '../state/selectors/conversations.js'; const { get, has, noop } = lodash; @@ -86,6 +85,7 @@ export type PropsType = { imageToBlurHash: typeof imageToBlurHash; onClose: () => unknown; onDone: (result: MediaEditorResultType) => unknown; + conversationSelector: GetConversationByIdType; } & Pick< CompositionInputProps, | 'draftText' @@ -168,7 +168,8 @@ export function MediaEditor({ ourConversationId, platform, sortedGroupMembers, - ...props + conversationSelector, + imageToBlurHash, }: PropsType): JSX.Element | null { const [fabricCanvas, setFabricCanvas] = useState(); const [image, setImage] = useState(new Image()); @@ -179,7 +180,6 @@ export function MediaEditor({ const [captionBodyRanges, setCaptionBodyRanges] = useState(draftBodyRanges); - const conversationSelector = useSelector(getConversationSelector); const hydratedBodyRanges = useMemo( () => hydrateRanges(captionBodyRanges ?? undefined, conversationSelector), [captionBodyRanges, conversationSelector] @@ -1408,7 +1408,7 @@ export function MediaEditor({ type: IMAGE_PNG, }); - blurHash = await props.imageToBlurHash(blob); + blurHash = await imageToBlurHash(blob); } catch (err) { onTryClose(); throw err; diff --git a/ts/components/ServerAlerts.tsx b/ts/components/ServerAlerts.tsx index c5f8807205..5d9c5f635c 100644 --- a/ts/components/ServerAlerts.tsx +++ b/ts/components/ServerAlerts.tsx @@ -2,11 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import { - getServerAlertToShow, - ServerAlert, - type ServerAlertsType, -} from '../util/handleServerAlerts.js'; +import { ServerAlert, type ServerAlertsType } from '../types/ServerAlert.js'; import type { WidthBreakpoint } from './_util.js'; import type { LocalizerType } from '../types/I18N.js'; import { CriticalIdlePrimaryDeviceDialog } from './CriticalIdlePrimaryDeviceDialog.js'; @@ -15,6 +11,8 @@ import { WarningIdlePrimaryDeviceDialog } from './WarningIdlePrimaryDeviceDialog export function getServerAlertDialog( alerts: ServerAlertsType | undefined, + getServerAlertToShow: (alerts: ServerAlertsType) => ServerAlert | null, + saveAlerts: (alerts: ServerAlertsType) => Promise, dialogProps: { containerWidthBreakpoint: WidthBreakpoint; i18n: LocalizerType; @@ -45,7 +43,7 @@ export function getServerAlertDialog( handleClose={ isDismissable ? async () => { - await window.storage.put('serverAlerts', { + await saveAlerts({ ...alerts, [ServerAlert.IDLE_PRIMARY_DEVICE]: { ...alert, diff --git a/ts/components/StoryCreator.tsx b/ts/components/StoryCreator.tsx index d20c2a1cc5..d04253fdba 100644 --- a/ts/components/StoryCreator.tsx +++ b/ts/components/StoryCreator.tsx @@ -93,10 +93,12 @@ export type PropsType = { | 'onTextTooLong' | 'platform' | 'sortedGroupMembers' + | 'conversationSelector' >; export function StoryCreator({ candidateConversations, + conversationSelector, debouncedMaybeGrabLinkPreview, distributionLists, file, @@ -263,6 +265,7 @@ export function StoryCreator({ isCreatingStory isFormattingEnabled={isFormattingEnabled} isSending={isSending} + conversationSelector={conversationSelector} onClose={onClose} onDone={({ contentType, diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 2862c1acc9..1df79d858e 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -96,7 +96,7 @@ import { getBadgeImageFileLocalPath } from '../../badges/getBadgeImageFileLocalP import { handleOutsideClick } from '../../util/handleOutsideClick.js'; import { isPaymentNotificationEvent } from '../../types/Payment.js'; import type { AnyPaymentEvent } from '../../types/Payment.js'; -import { getPaymentEventDescription } from '../../messages/helpers.js'; +import { getPaymentEventDescription } from '../../messages/payments.js'; import { PanelType } from '../../types/Panels.js'; import { isPollReceiveEnabled } from '../../types/Polls.js'; import type { PollWithResolvedVotersType } from '../../state/selectors/message.js'; diff --git a/ts/components/conversation/PaymentEventNotification.tsx b/ts/components/conversation/PaymentEventNotification.tsx index 05a0bd627e..b248430f71 100644 --- a/ts/components/conversation/PaymentEventNotification.tsx +++ b/ts/components/conversation/PaymentEventNotification.tsx @@ -8,7 +8,7 @@ import type { ConversationType } from '../../state/ducks/conversations.js'; import { SystemMessage } from './SystemMessage.js'; import { Emojify } from './Emojify.js'; import type { AnyPaymentEvent } from '../../types/Payment.js'; -import { getPaymentEventDescription } from '../../messages/helpers.js'; +import { getPaymentEventDescription } from '../../messages/payments.js'; export type PropsType = { event: AnyPaymentEvent; diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx index a34a6c29fd..4af3f98e03 100644 --- a/ts/components/conversation/Quote.tsx +++ b/ts/components/conversation/Quote.tsx @@ -24,10 +24,8 @@ import { getClassNamesFor } from '../../util/getClassNamesFor.js'; import { getCustomColorStyle } from '../../util/getCustomColorStyle.js'; import type { AnyPaymentEvent } from '../../types/Payment.js'; import { PaymentEventKind } from '../../types/Payment.js'; -import { - getPaymentEventNotificationText, - shouldTryToCopyFromQuotedMessage, -} from '../../messages/helpers.js'; +import { getPaymentEventNotificationText } from '../../messages/payments.js'; +import { shouldTryToCopyFromQuotedMessage } from '../../messages/helpers.js'; import { RenderLocation } from './MessageTextRenderer.js'; import type { QuotedAttachmentType } from '../../model-types.js'; diff --git a/ts/components/fun/data/segments.ts b/ts/components/fun/data/segments.ts index 1eee15741c..80bdaa97c5 100644 --- a/ts/components/fun/data/segments.ts +++ b/ts/components/fun/data/segments.ts @@ -1,6 +1,7 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { strictAssert } from '../../../util/assert.js'; +import type { fetchBytesViaProxy } from '../../../textsecure/WebAPI.js'; /** @internal Exported for testing */ export const _SEGMENT_SIZE_BUCKETS: ReadonlyArray = [ @@ -24,11 +25,10 @@ export type _SegmentRange = Readonly<{ async function fetchContentLength( url: string, + doFetchBytesViaProxy: typeof fetchBytesViaProxy, signal?: AbortSignal ): Promise { - const { messaging } = window.textsecure; - strictAssert(messaging, 'Missing window.textsecure.messaging'); - const { response } = await messaging.server.fetchBytesViaProxy({ + const { response } = await doFetchBytesViaProxy({ url, method: 'HEAD', signal, @@ -92,11 +92,10 @@ async function fetchSegment( url: string, segmentRange: _SegmentRange, contentLength: number, + doFetchBytesViaProxy: typeof fetchBytesViaProxy, signal?: AbortSignal ): Promise { - const { messaging } = window.textsecure; - strictAssert(messaging, 'Missing window.textsecure.messaging'); - const { data, response } = await messaging.server.fetchBytesViaProxy({ + const { data, response } = await doFetchBytesViaProxy({ method: 'GET', url, signal, @@ -140,14 +139,25 @@ async function fetchSegment( export async function fetchInSegments( url: string, + doFetchBytesViaProxy: typeof fetchBytesViaProxy, signal?: AbortSignal ): Promise { - const contentLength = await fetchContentLength(url, signal); + const contentLength = await fetchContentLength( + url, + doFetchBytesViaProxy, + signal + ); const segmentSize = _getSegmentSize(contentLength); const segmentRanges = _getSegmentRanges(contentLength, segmentSize); const segmentBuffers = await Promise.all( segmentRanges.map(segmentRange => { - return fetchSegment(url, segmentRange, contentLength, signal); + return fetchSegment( + url, + segmentRange, + contentLength, + doFetchBytesViaProxy, + signal + ); }) ); return new Blob(segmentBuffers); diff --git a/ts/components/fun/data/tenor.ts b/ts/components/fun/data/tenor.ts index 791d470e41..e7dfa40c6c 100644 --- a/ts/components/fun/data/tenor.ts +++ b/ts/components/fun/data/tenor.ts @@ -5,6 +5,10 @@ import { z } from 'zod'; import type { Simplify } from 'type-fest'; import { strictAssert } from '../../../util/assert.js'; import { parseUnknown } from '../../../util/schemas.js'; +import { + fetchJsonViaProxy, + fetchBytesViaProxy, +} from '../../../textsecure/WebAPI.js'; import { fetchInSegments } from './segments.js'; const BASE_URL = 'https://tenor.googleapis.com/v2'; @@ -196,9 +200,6 @@ export async function tenor( params: Omit, signal?: AbortSignal ): Promise { - const { messaging } = window.textsecure; - strictAssert(messaging, 'Missing window.textsecure.messaging'); - const schema = ResponseSchemaMap[path]; strictAssert(schema, 'Missing schema'); @@ -216,7 +217,7 @@ export async function tenor( url.searchParams.set(key, param); } - const response = await messaging.server.fetchJsonViaProxy({ + const response = await fetchJsonViaProxy({ method: 'GET', url: url.toString(), signal, @@ -229,5 +230,5 @@ export function tenorDownload( tenorCdnUrl: string, signal?: AbortSignal ): Promise { - return fetchInSegments(tenorCdnUrl, signal); + return fetchInSegments(tenorCdnUrl, fetchBytesViaProxy, signal); } diff --git a/ts/groups.ts b/ts/groups.ts index aec35bd8cb..e9a81effb6 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -54,8 +54,17 @@ import type { GroupCredentialsType, GroupLogResponseType, } from './textsecure/WebAPI.js'; +import { + createGroup, + getGroup, + getGroupAvatar, + getGroupLog, + getGroupFromLink, + getExternalGroupCredential, + modifyGroup, + uploadGroupAvatar, +} from './textsecure/WebAPI.js'; import { HTTPError } from './types/HTTPError.js'; -import type MessageSender from './textsecure/SendMessage.js'; import { CURRENT_SCHEMA_VERSION as MAX_MESSAGE_SCHEMA } from './types/Message2.js'; import type { ConversationModel } from './models/conversations.js'; import { getGroupSizeHardLimit } from './groups/limits.js'; @@ -106,6 +115,7 @@ import { isConversationAccepted, isTrustedContact, } from './util/isConversationAccepted.js'; +import { itemStorage } from './textsecure/Storage.js'; const { compact, difference, flatten, fromPairs, isNumber, omit, values } = lodash; @@ -212,8 +222,7 @@ export async function getPreJoinGroupInfo( logId: `getPreJoinInfo/groupv2(${data.id})`, publicParams: Bytes.toBase64(data.publicParams), secretParams: Bytes.toBase64(data.secretParams), - request: (sender, options) => - sender.getGroupFromLink(inviteLinkPasswordBase64, options), + request: options => getGroupFromLink(inviteLinkPasswordBase64, options), }); } @@ -313,8 +322,7 @@ async function uploadAvatar(options: { logId: `uploadGroupAvatar/${logId}`, publicParams, secretParams, - request: (sender, requestOptions) => - sender.uploadGroupAvatar(ciphertext, requestOptions), + request: requestOptions => uploadGroupAvatar(ciphertext, requestOptions), }); return { @@ -466,7 +474,7 @@ function buildGroupProto( return member; }); - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const ourAciCipherTextBuffer = encryptServiceId(clientZkGroupCipher, ourAci); @@ -533,7 +541,7 @@ export async function buildAddMembersChange( ); const clientZkGroupCipher = getClientZkGroupCipher(secretParams); - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const ourAciCipherTextBuffer = encryptServiceId(clientZkGroupCipher, ourAci); const now = Date.now(); @@ -1306,8 +1314,7 @@ async function uploadGroupChange({ logId: `uploadGroupChange/${logId}`, publicParams: groupPublicParamsBase64, secretParams: groupSecretParamsBase64, - request: (sender, options) => - sender.modifyGroup(actions, options, inviteLinkPassword), + request: options => modifyGroup(actions, options, inviteLinkPassword), }); } @@ -1565,19 +1572,12 @@ async function makeRequestWithCredentials({ logId: string; publicParams: string; secretParams: string; - request: (sender: MessageSender, options: GroupCredentialsType) => Promise; + request: (options: GroupCredentialsType) => Promise; }): Promise { const groupCredentials = getCheckedGroupCredentialsForToday( `makeRequestWithCredentials/${logId}` ); - const sender = window.textsecure.messaging; - if (!sender) { - throw new Error( - `makeRequestWithCredentials/${logId}: textsecure.messaging is not available!` - ); - } - log.info(`makeRequestWithCredentials/${logId}: starting`); const todayOptions = getGroupCredentials({ @@ -1587,7 +1587,7 @@ async function makeRequestWithCredentials({ serverPublicParamsBase64: window.getServerPublicParams(), }); - return request(sender, todayOptions); + return request(todayOptions); } export async function fetchMembershipProof({ @@ -1611,7 +1611,7 @@ export async function fetchMembershipProof({ logId: 'fetchMembershipProof', publicParams, secretParams, - request: (sender, options) => sender.getGroupMembershipToken(options), + request: options => getExternalGroupCredential(options), }); return dropNull(response.token); } @@ -1653,7 +1653,7 @@ export async function createGroupV2( const secretParams = Bytes.toBase64(fields.secretParams); const publicParams = Bytes.toBase64(fields.publicParams); - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const ourConversation = window.ConversationController.getOurConversationOrThrow(); @@ -1763,8 +1763,7 @@ export async function createGroupV2( logId: `createGroupV2/${logId}`, publicParams, secretParams, - request: (sender, requestOptions) => - sender.createGroup(groupProto, requestOptions), + request: requestOptions => createGroup(groupProto, requestOptions), }); const { groupSendEndorsementResponse } = groupResponse; @@ -1936,7 +1935,7 @@ export async function hasV1GroupBeenMigrated( logId: `getGroup/${logId}`, publicParams: Bytes.toBase64(fields.publicParams), secretParams: Bytes.toBase64(fields.secretParams), - request: (sender, options) => sender.getGroup(options), + request: options => getGroup(options), }); return true; } catch (error) { @@ -2026,7 +2025,7 @@ export async function getGroupMigrationMembers( ); } - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); let areWeMember = false; let areWeInvited = false; @@ -2295,7 +2294,7 @@ export async function initiateMigrationToGroupV2( logId: `initiateMigrationToGroupV2/${logId}`, publicParams, secretParams, - request: (sender, options) => sender.createGroup(groupProto, options), + request: options => createGroup(groupProto, options), }); groupSendEndorsementResponse = @@ -2334,8 +2333,8 @@ export async function initiateMigrationToGroupV2( }, }); - if (window.storage.blocked.isGroupBlocked(previousGroupV1Id)) { - await window.storage.blocked.addBlockedGroup(groupId); + if (itemStorage.blocked.isGroupBlocked(previousGroupV1Id)) { + await itemStorage.blocked.addBlockedGroup(groupId); } // Save these most recent updates to conversation @@ -2421,8 +2420,8 @@ export function buildMigrationBubble( previousGroupV1MembersIds: ReadonlyArray, newAttributes: ConversationAttributesType ): GroupChangeMessageType { - const ourAci = window.storage.user.getCheckedAci(); - const ourPni = window.storage.user.getPni(); + const ourAci = itemStorage.user.getCheckedAci(); + const ourPni = itemStorage.user.getPni(); const ourConversationId = window.ConversationController.getOurConversationId(); @@ -2583,7 +2582,7 @@ export async function respondToGroupV2Migration({ ); } - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const wereWePreviouslyAMember = conversation.hasMember(ourAci); // Derive GroupV2 fields @@ -2637,8 +2636,8 @@ export async function respondToGroupV2Migration({ logId: `getGroupLog/${logId}`, publicParams, secretParams, - request: (sender, options) => - sender.getGroupLog( + request: options => + getGroupLog( { startVersion: 0, includeFirstState: true, @@ -2665,7 +2664,7 @@ export async function respondToGroupV2Migration({ logId: `getGroup/${logId}`, publicParams, secretParams, - request: (sender, options) => sender.getGroup(options), + request: options => getGroup(options), }); setLastSuccessfulGroupFetch(conversation.id, fetchedAt); @@ -2678,15 +2677,15 @@ export async function respondToGroupV2Migration({ `respondToGroupV2Migration/${logId}: Failed to access state endpoint; user is no longer part of group` ); - if (window.storage.blocked.isGroupBlocked(previousGroupV1Id)) { - await window.storage.blocked.addBlockedGroup(groupId); + if (itemStorage.blocked.isGroupBlocked(previousGroupV1Id)) { + await itemStorage.blocked.addBlockedGroup(groupId); } if (wereWePreviouslyAMember) { log.info( `respondToGroupV2Migration/${logId}: Upgrading group with migration/removed events` ); - const ourNumber = window.textsecure.storage.user.getNumber(); + const ourNumber = itemStorage.user.getNumber(); await updateGroup({ conversation, receivedAt, @@ -2817,8 +2816,8 @@ export async function respondToGroupV2Migration({ }, }); - if (window.storage.blocked.isGroupBlocked(previousGroupV1Id)) { - await window.storage.blocked.addBlockedGroup(groupId); + if (itemStorage.blocked.isGroupBlocked(previousGroupV1Id)) { + await itemStorage.blocked.addBlockedGroup(groupId); } // Save these most recent updates to conversation @@ -2979,8 +2978,8 @@ async function updateGroup( const logId = conversation.idForLogging(); const { newAttributes, groupChangeMessages, newProfileKeys } = updates; - const ourAci = window.textsecure.storage.user.getCheckedAci(); - const ourPni = window.textsecure.storage.user.getPni(); + const ourAci = itemStorage.user.getCheckedAci(); + const ourPni = itemStorage.user.getPni(); const wasMemberOrPending = conversation.hasMember(ourAci) || @@ -3287,7 +3286,7 @@ async function appendChangeMessages( `appendChangeMessages/${logId}: processing ${messages.length} messages` ); - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); let lastMessage = await DataReader.getLastConversationMessage({ conversationId: conversation.id, @@ -3396,7 +3395,7 @@ async function getGroupUpdates({ const currentRevision = group.revision; const isFirstFetch = !isNumber(group.revision); - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const isInitialCreationMessage = isFirstFetch && newRevision === 0; const weAreAwaitingApproval = (group.pendingAdminApprovalV2 || []).some( @@ -3598,7 +3597,7 @@ async function updateGroupViaPreJoinInfo({ group: ConversationAttributesType; }): Promise { const logId = idForLogging(group.groupId); - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const { publicParams, secretParams } = group; if (!secretParams) { @@ -3618,8 +3617,7 @@ async function updateGroupViaPreJoinInfo({ logId: `getPreJoinInfo/${logId}`, publicParams, secretParams, - request: (sender, options) => - sender.getGroupFromLink(inviteLinkPassword, options), + request: options => getGroupFromLink(inviteLinkPassword, options), }); const approvalRequired = @@ -3691,7 +3689,7 @@ async function updateGroupViaState({ logId: `getGroup/${logId}`, publicParams, secretParams, - request: (sender, requestOptions) => sender.getGroup(requestOptions), + request: requestOptions => getGroup(requestOptions), }); setLastSuccessfulGroupFetch(id, fetchedAt); @@ -3925,8 +3923,8 @@ async function updateGroupViaLogs({ secretParams, // eslint-disable-next-line no-loop-func - request: (sender, requestOptions) => - sender.getGroupLog( + request: requestOptions => + getGroupLog( { startVersion: revisionToFetch, includeFirstState, @@ -4017,8 +4015,8 @@ async function generateLeftGroupChanges( ): Promise { const logId = idForLogging(group.groupId); log.info(`generateLeftGroupChanges/${logId}: Starting...`); - const ourAci = window.storage.user.getCheckedAci(); - const ourPni = window.storage.user.getCheckedPni(); + const ourAci = itemStorage.user.getCheckedAci(); + const ourPni = itemStorage.user.getCheckedPni(); const { masterKey, groupInviteLinkPassword } = group; let { revision } = group; @@ -4217,7 +4215,7 @@ async function integrateGroupChange({ } const isFirstFetch = !isNumber(group.revision); - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const weAreAwaitingApproval = (group.pendingAdminApprovalV2 || []).some( item => item.aci === ourAci ); @@ -4444,8 +4442,8 @@ function extractDiffs({ }): Array { const logId = idForLogging(old.groupId); const details: Array = []; - const ourAci = window.storage.user.getCheckedAci(); - const ourPni = window.storage.user.getPni(); + const ourAci = itemStorage.user.getCheckedAci(); + const ourPni = itemStorage.user.getPni(); const ACCESS_ENUM = Proto.AccessControl.AccessRequired; let areWeInGroup = false; @@ -4821,7 +4819,7 @@ function extractDiffs({ details: [ { type: 'pending-add-one', - serviceId: window.storage.user.getCheckedServiceId( + serviceId: itemStorage.user.getCheckedServiceId( serviceIdKindInvitedToGroup ), }, @@ -4999,7 +4997,7 @@ async function applyGroupChange({ sourceServiceId: ServiceIdString; }): Promise { const logId = idForLogging(group.groupId); - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const ACCESS_ENUM = Proto.AccessControl.AccessRequired; const MEMBER_ROLE_ENUM = Proto.Member.Role; @@ -5561,14 +5559,7 @@ export async function decryptGroupAvatar( avatarKey: string, secretParamsBase64: string ): Promise { - const sender = window.textsecure.messaging; - if (!sender) { - throw new Error( - 'decryptGroupAvatar: textsecure.messaging is not available!' - ); - } - - const ciphertext = await sender.getGroupAvatar(avatarKey); + const ciphertext = await getGroupAvatar(avatarKey); const clientZkGroupCipher = getClientZkGroupCipher(secretParamsBase64); const plaintext = decryptGroupBlob(clientZkGroupCipher, ciphertext); const blob = Proto.GroupAttributeBlob.decode(plaintext); @@ -5786,7 +5777,7 @@ async function applyGroupState({ // Optimization: we assume we have left the group unless we are found in members result.left = true; - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); // members const wasPreviouslyAMember = (result.membersV2 || []).some( diff --git a/ts/groups/joinViaLink.ts b/ts/groups/joinViaLink.ts index 1a0eae75cb..ff689d53ed 100644 --- a/ts/groups/joinViaLink.ts +++ b/ts/groups/joinViaLink.ts @@ -35,6 +35,7 @@ import { dropNull } from '../util/dropNull.js'; import { getLocalAttachmentUrl } from '../util/getLocalAttachmentUrl.js'; import { type Loadable, LoadingState } from '../util/loadable.js'; import { missingCaseError } from '../util/missingCaseError.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('joinViaLink'); @@ -70,7 +71,7 @@ export async function joinViaLink(value: string): Promise { const existingConversation = window.ConversationController.get(id) || window.ConversationController.getByDerivedGroupV2Id(id); - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); if (existingConversation && existingConversation.hasMember(ourAci)) { log.warn(`${logId}: Already a member of group, opening conversation`); diff --git a/ts/hooks/useIsOnline.ts b/ts/hooks/useIsOnline.ts deleted file mode 100644 index 964d047169..0000000000 --- a/ts/hooks/useIsOnline.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { useEffect, useState } from 'react'; - -function getOnlineStatus(): boolean { - if (window.textsecure) { - return window.textsecure.server?.isOnline() ?? true; - } - - // Only for storybook - return navigator.onLine; -} - -export function useIsOnline(): boolean { - const [isOnline, setIsOnline] = useState(getOnlineStatus()); - - useEffect(() => { - const update = () => { - setIsOnline(getOnlineStatus()); - }; - - update(); - - window.Whisper.events.on('online', update); - window.Whisper.events.on('offline', update); - - return () => { - window.Whisper.events.off('online', update); - window.Whisper.events.off('offline', update); - }; - }, []); - - return isOnline; -} diff --git a/ts/jobs/AttachmentBackupManager.ts b/ts/jobs/AttachmentBackupManager.ts index e7617d5541..6dfb0b6b3f 100644 --- a/ts/jobs/AttachmentBackupManager.ts +++ b/ts/jobs/AttachmentBackupManager.ts @@ -44,7 +44,7 @@ import { getMediaNameForAttachmentThumbnail, } from '../services/backups/util/mediaId.js'; import { fromBase64, toBase64 } from '../Bytes.js'; -import type { WebAPIType } from '../textsecure/WebAPI.js'; +import { backupMediaBatch as doBackupMediaBatch } from '../textsecure/WebAPI.js'; import type { AttachmentType } from '../types/Attachment.js'; import { canAttachmentHaveThumbnail, @@ -186,7 +186,7 @@ class FileNotFoundOnTransitTierError extends Error {} type RunAttachmentBackupJobDependenciesType = { getAbsoluteAttachmentPath: typeof window.Signal.Migrations.getAbsoluteAttachmentPath; - backupMediaBatch?: WebAPIType['backupMediaBatch']; + backupMediaBatch?: typeof doBackupMediaBatch; backupsService: BackupsService; encryptAndUploadAttachment: typeof encryptAndUploadAttachment; decryptAttachmentV2ToSink: typeof decryptAttachmentV2ToSink; @@ -202,7 +202,7 @@ export async function runAttachmentBackupJob( getAbsoluteAttachmentPath: window.Signal.Migrations.getAbsoluteAttachmentPath, backupsService, - backupMediaBatch: window.textsecure.server?.backupMediaBatch, + backupMediaBatch: doBackupMediaBatch, encryptAndUploadAttachment, decryptAttachmentV2ToSink, } @@ -574,7 +574,7 @@ async function copyToBackupTier({ macKey: Uint8Array; aesKey: Uint8Array; dependencies: { - backupMediaBatch?: WebAPIType['backupMediaBatch']; + backupMediaBatch?: typeof doBackupMediaBatch; backupsService: BackupsService; }; }): Promise<{ cdnNumberOnBackup: number }> { diff --git a/ts/jobs/AttachmentDownloadManager.ts b/ts/jobs/AttachmentDownloadManager.ts index b9994db1de..73d191dbea 100644 --- a/ts/jobs/AttachmentDownloadManager.ts +++ b/ts/jobs/AttachmentDownloadManager.ts @@ -76,6 +76,7 @@ import { isOlderThan } from '../util/timestamp.js'; import { getMessageQueueTime as doGetMessageQueueTime } from '../util/getMessageQueueTime.js'; import { JobCancelReason } from './types.js'; import { isAbortError } from '../util/isAbortError.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { noop, omit, throttle } = lodash; @@ -217,11 +218,11 @@ export class AttachmentDownloadManager extends JobManager { - if (!window.storage.get('backupMediaDownloadPaused')) { + if (!itemStorage.get('backupMediaDownloadPaused')) { await Promise.all([ - window.storage.put('backupMediaDownloadPaused', true), + itemStorage.put('backupMediaDownloadPaused', true), // Show the banner to allow users to resume from the left pane - window.storage.put('backupMediaDownloadBannerDismissed', false), + itemStorage.put('backupMediaDownloadBannerDismissed', false), ]); } window.reduxActions.globalModals.showLowDiskSpaceBackupImportModal( @@ -240,7 +241,7 @@ export class AttachmentDownloadManager extends JobManager { await AttachmentDownloadManager.saveBatchedJobs(); - await window.storage.put('attachmentDownloadManagerIdled', false); + await itemStorage.put('attachmentDownloadManagerIdled', false); await AttachmentDownloadManager.instance.start(); drop( AttachmentDownloadManager.waitForIdle(async () => { await updateBackupMediaDownloadProgress( DataReader.getBackupAttachmentDownloadProgress ); - await window.storage.put('attachmentDownloadManagerIdled', true); + await itemStorage.put('attachmentDownloadManagerIdled', true); }) ); } diff --git a/ts/jobs/AttachmentLocalBackupManager.ts b/ts/jobs/AttachmentLocalBackupManager.ts index 25326cb3ae..869e05f09b 100644 --- a/ts/jobs/AttachmentLocalBackupManager.ts +++ b/ts/jobs/AttachmentLocalBackupManager.ts @@ -27,7 +27,7 @@ import { } from '../types/AttachmentBackup.js'; import { isInCall as isInCallSelector } from '../state/selectors/calling.js'; import { encryptAndUploadAttachment } from '../util/uploadAttachment.js'; -import type { WebAPIType } from '../textsecure/WebAPI.js'; +import { backupMediaBatch as doBackupMediaBatch } from '../textsecure/WebAPI.js'; import { getLocalBackupDirectoryForMediaName, getLocalBackupPathForMediaName, @@ -165,7 +165,7 @@ class AttachmentPermanentlyMissingError extends Error {} type RunAttachmentBackupJobDependenciesType = { getAbsoluteAttachmentPath: typeof window.Signal.Migrations.getAbsoluteAttachmentPath; - backupMediaBatch?: WebAPIType['backupMediaBatch']; + backupMediaBatch?: typeof doBackupMediaBatch; backupsService: BackupsService; encryptAndUploadAttachment: typeof encryptAndUploadAttachment; decryptAttachmentV2ToSink: typeof decryptAttachmentV2ToSink; @@ -181,7 +181,7 @@ export async function runAttachmentBackupJob( getAbsoluteAttachmentPath: window.Signal.Migrations.getAbsoluteAttachmentPath, backupsService, - backupMediaBatch: window.textsecure.server?.backupMediaBatch, + backupMediaBatch: doBackupMediaBatch, encryptAndUploadAttachment, decryptAttachmentV2ToSink, } diff --git a/ts/jobs/conversationJobQueue.ts b/ts/jobs/conversationJobQueue.ts index 152b19e079..6e415440ef 100644 --- a/ts/jobs/conversationJobQueue.ts +++ b/ts/jobs/conversationJobQueue.ts @@ -36,7 +36,10 @@ import { missingCaseError } from '../util/missingCaseError.js'; import { explodePromise } from '../util/explodePromise.js'; import type { Job } from './Job.js'; import type { ParsedJob, StoredJob } from './types.js'; -import type SendMessage from '../textsecure/SendMessage.js'; +import { + type MessageSender, + messageSender, +} from '../textsecure/SendMessage.js'; import type { ServiceIdString } from '../types/ServiceId.js'; import { commonShouldJobContinue } from './helpers/commonShouldJobContinue.js'; import { sleeper } from '../util/sleeper.js'; @@ -267,7 +270,7 @@ export type ConversationQueueJobData = z.infer< export type ConversationQueueJobBundle = { isFinalAttempt: boolean; log: LoggerType; - messaging: SendMessage; + messaging: MessageSender; shouldContinue: boolean; timeRemaining: number; timestamp: number; @@ -914,13 +917,8 @@ export class ConversationJobQueue extends JobQueue { throw missingCaseError(verificationData); } - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error('messaging interface is not available!'); - } - const jobBundle: ConversationQueueJobBundle = { - messaging, + messaging: messageSender, isFinalAttempt, shouldContinue, timeRemaining, diff --git a/ts/jobs/deleteDownloadsJobQueue.ts b/ts/jobs/deleteDownloadsJobQueue.ts index 75dab28a81..0324de4665 100644 --- a/ts/jobs/deleteDownloadsJobQueue.ts +++ b/ts/jobs/deleteDownloadsJobQueue.ts @@ -14,6 +14,7 @@ import type { LoggerType } from '../types/Logging.js'; import { commonShouldJobContinue } from './helpers/commonShouldJobContinue.js'; import { DAY } from '../util/durations/index.js'; import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { omit } = lodash; @@ -42,7 +43,7 @@ export class DeleteDownloadsJobQueue extends JobQueue { { attempt, log }: Readonly<{ attempt: number; log: LoggerType }> ): Promise { await new Promise(resolve => { - window.storage.onready(resolve); + itemStorage.onready(resolve); }); const timeRemaining = timestamp + MAX_RETRY_TIME - Date.now(); diff --git a/ts/jobs/helpers/attachmentBackfill.ts b/ts/jobs/helpers/attachmentBackfill.ts index 803955a8c6..fca0dab346 100644 --- a/ts/jobs/helpers/attachmentBackfill.ts +++ b/ts/jobs/helpers/attachmentBackfill.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { AttachmentBackfillResponseSyncEvent } from '../../textsecure/messageReceiverEvents.js'; -import MessageSender from '../../textsecure/SendMessage.js'; +import { MessageSender } from '../../textsecure/SendMessage.js'; import { createLogger } from '../../logging/log.js'; import type { ReadonlyMessageAttributesType } from '../../model-types.d.ts'; import type { AttachmentType } from '../../types/Attachment.js'; diff --git a/ts/jobs/helpers/commonShouldJobContinue.ts b/ts/jobs/helpers/commonShouldJobContinue.ts index 17dec11e15..909e0f0388 100644 --- a/ts/jobs/helpers/commonShouldJobContinue.ts +++ b/ts/jobs/helpers/commonShouldJobContinue.ts @@ -2,10 +2,12 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { LoggerType } from '../../types/Logging.js'; +import { isOnline } from '../../textsecure/WebAPI.js'; import { waitForOnline } from '../../util/waitForOnline.js'; import { exponentialBackoffSleepTime } from '../../util/exponentialBackoff.js'; import { isDone as isDeviceLinked } from '../../util/registration.js'; import { sleeper } from '../../util/sleeper.js'; +import { itemStorage } from '../../textsecure/Storage.js'; export async function commonShouldJobContinue({ attempt, @@ -25,7 +27,7 @@ export async function commonShouldJobContinue({ try { if (isDeviceLinked()) { - await waitForOnline({ timeout: timeRemaining }); + await waitForOnline({ server: { isOnline }, timeout: timeRemaining }); } } catch (err: unknown) { log.info("didn't come online in time, giving up"); @@ -33,7 +35,7 @@ export async function commonShouldJobContinue({ } await new Promise(resolve => { - window.storage.onready(resolve); + itemStorage.onready(resolve); }); if (!isDeviceLinked()) { diff --git a/ts/jobs/helpers/sendNullMessage.ts b/ts/jobs/helpers/sendNullMessage.ts index 86f9eb749e..87f12f8201 100644 --- a/ts/jobs/helpers/sendNullMessage.ts +++ b/ts/jobs/helpers/sendNullMessage.ts @@ -22,20 +22,21 @@ import { OutgoingIdentityKeyError, UnregisteredUserError, } from '../../textsecure/Errors.js'; -import MessageSender from '../../textsecure/SendMessage.js'; +import { MessageSender } from '../../textsecure/SendMessage.js'; import { sendToGroup } from '../../util/sendToGroup.js'; +import { itemStorage } from '../../textsecure/Storage.js'; async function clearResetsTracking(idForTracking: string | undefined) { if (!idForTracking) { return; } - const sessionResets = window.storage.get( + const sessionResets = itemStorage.get( 'sessionResets', {} as SessionResetsType ); delete sessionResets[idForTracking]; - await window.storage.put('sessionResets', sessionResets); + await itemStorage.put('sessionResets', sessionResets); } export async function sendNullMessage( diff --git a/ts/jobs/helpers/sendProfileKey.ts b/ts/jobs/helpers/sendProfileKey.ts index 0fb3175d46..b0c1e5d2e3 100644 --- a/ts/jobs/helpers/sendProfileKey.ts +++ b/ts/jobs/helpers/sendProfileKey.ts @@ -34,6 +34,7 @@ import { } from '../../textsecure/Errors.js'; import { shouldSendToConversation } from './shouldSendToConversation.js'; import { sendToGroup } from '../../util/sendToGroup.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { isNumber } = lodash; @@ -138,7 +139,7 @@ export async function sendProfileKey( log.error('No revision provided, but conversation is GroupV2'); } - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); if (!conversation.hasMember(ourAci)) { log.info( `We are not part of group ${conversation.idForLogging()}; refusing to send` diff --git a/ts/jobs/helpers/sendReaction.ts b/ts/jobs/helpers/sendReaction.ts index 58f2edbbf5..6c0ab01e1a 100644 --- a/ts/jobs/helpers/sendReaction.ts +++ b/ts/jobs/helpers/sendReaction.ts @@ -42,6 +42,7 @@ import type { LoggerType } from '../../types/Logging.js'; import { sendToGroup } from '../../util/sendToGroup.js'; import { hydrateStoryContext } from '../../util/hydrateStoryContext.js'; import { send, sendSyncMessageOnly } from '../../messages/send.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { isNumber } = lodash; @@ -57,7 +58,7 @@ export async function sendReaction( data: ReactionJobData ): Promise { const { messageId, revision } = data; - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); await window.ConversationController.load(); diff --git a/ts/jobs/helpers/syncHelpers.ts b/ts/jobs/helpers/syncHelpers.ts index f807041674..a276afbccd 100644 --- a/ts/jobs/helpers/syncHelpers.ts +++ b/ts/jobs/helpers/syncHelpers.ts @@ -15,7 +15,10 @@ import { isRecord } from '../../util/isRecord.js'; import { commonShouldJobContinue } from './commonShouldJobContinue.js'; import { handleCommonJobRequestError } from './handleCommonJobRequestError.js'; import { missingCaseError } from '../../util/missingCaseError.js'; -import type SendMessage from '../../textsecure/SendMessage.js'; +import { + type MessageSender, + messageSender, +} from '../../textsecure/SendMessage.js'; const { chunk } = lodash; @@ -132,24 +135,19 @@ export async function runSyncJob({ syncMessage: true, }); - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error('messaging is not available!'); - } - let doSync: - | SendMessage['syncReadMessages'] - | SendMessage['syncView'] - | SendMessage['syncViewOnceOpen']; + | MessageSender['syncReadMessages'] + | MessageSender['syncView'] + | MessageSender['syncViewOnceOpen']; switch (type) { case SyncTypeList.View: - doSync = messaging.syncView.bind(messaging); + doSync = messageSender.syncView.bind(messageSender); break; case SyncTypeList.Read: - doSync = messaging.syncReadMessages.bind(messaging); + doSync = messageSender.syncReadMessages.bind(messageSender); break; case SyncTypeList.ViewOnceOpen: - doSync = messaging.syncViewOnceOpen.bind(messaging); + doSync = messageSender.syncViewOnceOpen.bind(messageSender); break; default: { throw missingCaseError(type); diff --git a/ts/jobs/initializeAllJobQueues.ts b/ts/jobs/initializeAllJobQueues.ts index 03da108fae..376b4a354c 100644 --- a/ts/jobs/initializeAllJobQueues.ts +++ b/ts/jobs/initializeAllJobQueues.ts @@ -1,7 +1,7 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { WebAPIType } from '../textsecure/WebAPI.js'; +import type { reportMessage, isOnline } from '../textsecure/WebAPI.js'; import { drop } from '../util/drop.js'; import { CallLinkFinalizeDeleteManager } from './CallLinkFinalizeDeleteManager.js'; import { chatFolderCleanupService } from '../services/expiring/chatFolderCleanupService.js'; @@ -16,13 +16,18 @@ import { singleProtoJobQueue } from './singleProtoJobQueue.js'; import { viewOnceOpenJobQueue } from './viewOnceOpenJobQueue.js'; import { viewSyncJobQueue } from './viewSyncJobQueue.js'; +type ServerType = { + reportMessage: typeof reportMessage; + isOnline: typeof isOnline; +}; + /** * Start all of the job queues. Should be called when the database is ready. */ export function initializeAllJobQueues({ server, }: { - server: WebAPIType; + server: ServerType; }): void { reportSpamJobQueue.initialize({ server }); diff --git a/ts/jobs/removeStorageKeyJobQueue.ts b/ts/jobs/removeStorageKeyJobQueue.ts index 9e37235a8d..a385a8b06a 100644 --- a/ts/jobs/removeStorageKeyJobQueue.ts +++ b/ts/jobs/removeStorageKeyJobQueue.ts @@ -8,6 +8,7 @@ import { JobQueue } from './JobQueue.js'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore.js'; import { parseUnknown } from '../util/schemas.js'; +import { itemStorage } from '../textsecure/Storage.js'; const removeStorageKeyJobDataSchema = z.object({ key: z.enum([ @@ -32,10 +33,10 @@ export class RemoveStorageKeyJobQueue extends JobQueue typeof JOB_STATUS.NEEDS_RETRY | undefined > { await new Promise(resolve => { - window.storage.onready(resolve); + itemStorage.onready(resolve); }); - await window.storage.remove(data.key); + await itemStorage.remove(data.key); return undefined; } diff --git a/ts/jobs/reportSpamJobQueue.ts b/ts/jobs/reportSpamJobQueue.ts index 0675b4f756..35b1d4612a 100644 --- a/ts/jobs/reportSpamJobQueue.ts +++ b/ts/jobs/reportSpamJobQueue.ts @@ -14,10 +14,11 @@ import type { JOB_STATUS } from './JobQueue.js'; import { JobQueue } from './JobQueue.js'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore.js'; import { parseIntWithFallback } from '../util/parseIntWithFallback.js'; -import type { WebAPIType } from '../textsecure/WebAPI.js'; +import type { reportMessage, isOnline } from '../textsecure/WebAPI.js'; import { HTTPError } from '../types/HTTPError.js'; import { sleeper } from '../util/sleeper.js'; import { parseUnknown } from '../util/schemas.js'; +import { itemStorage } from '../textsecure/Storage.js'; const RETRY_WAIT_TIME = durations.MINUTE; const RETRYABLE_4XX_FAILURE_STATUSES = new Set([ @@ -37,10 +38,15 @@ const reportSpamJobDataSchema = z.object({ export type ReportSpamJobData = z.infer; -export class ReportSpamJobQueue extends JobQueue { - #server?: WebAPIType; +type ServerType = Readonly<{ + reportMessage: typeof reportMessage; + isOnline: typeof isOnline; +}>; - public initialize({ server }: { server: WebAPIType }): void { +export class ReportSpamJobQueue extends JobQueue { + #server?: ServerType; + + public initialize({ server }: { server: ServerType }): void { this.#server = server; } @@ -55,7 +61,7 @@ export class ReportSpamJobQueue extends JobQueue { const { aci: senderAci, token, serverGuids } = data; await new Promise(resolve => { - window.storage.onready(resolve); + itemStorage.onready(resolve); }); if (!isDeviceLinked()) { @@ -63,11 +69,11 @@ export class ReportSpamJobQueue extends JobQueue { return undefined; } - await waitForOnline(); - const server = this.#server; strictAssert(server !== undefined, 'ReportSpamJobQueue not initialized'); + await waitForOnline({ server }); + try { await Promise.all( map(serverGuids, serverGuid => diff --git a/ts/jobs/singleProtoJobQueue.ts b/ts/jobs/singleProtoJobQueue.ts index 7a48763fa7..d3a10fa1ac 100644 --- a/ts/jobs/singleProtoJobQueue.ts +++ b/ts/jobs/singleProtoJobQueue.ts @@ -17,7 +17,10 @@ import { SignalService as Proto } from '../protobuf/index.js'; import { handleMessageSend } from '../util/handleMessageSend.js'; import { getSendOptions } from '../util/getSendOptions.js'; import type { SingleProtoJobData } from '../textsecure/SendMessage.js'; -import { singleProtoJobDataSchema } from '../textsecure/SendMessage.js'; +import { + singleProtoJobDataSchema, + messageSender, +} from '../textsecure/SendMessage.js'; import { handleMultipleSendErrors, maybeExpandErrors, @@ -111,14 +114,9 @@ export class SingleProtoJobQueue extends JobQueue { syncMessage: isSyncMessage, }); - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error('messaging is not available!'); - } - try { await handleMessageSend( - messaging.sendIndividualProto({ + messageSender.sendIndividualProto({ contentHint, serviceId, options, diff --git a/ts/messageModifiers/Deletes.ts b/ts/messageModifiers/Deletes.ts index 867fd28348..173689d4f3 100644 --- a/ts/messageModifiers/Deletes.ts +++ b/ts/messageModifiers/Deletes.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { MessageAttributesType } from '../model-types.d.ts'; -import { getAuthorId } from '../messages/helpers.js'; +import { getAuthorId } from '../messages/sources.js'; import { DataReader } from '../sql/Client.js'; import { createLogger } from '../logging/log.js'; import * as Errors from '../types/errors.js'; diff --git a/ts/messageModifiers/Edits.ts b/ts/messageModifiers/Edits.ts index 7e3262ae69..ee6b2e055c 100644 --- a/ts/messageModifiers/Edits.ts +++ b/ts/messageModifiers/Edits.ts @@ -6,7 +6,7 @@ import * as Errors from '../types/errors.js'; import { createLogger } from '../logging/log.js'; import { DataReader } from '../sql/Client.js'; import { drop } from '../util/drop.js'; -import { getAuthorId } from '../messages/helpers.js'; +import { getAuthorId } from '../messages/sources.js'; import { handleEditMessage } from '../util/handleEditMessage.js'; import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp.js'; import { diff --git a/ts/messageModifiers/MessageReceipts.ts b/ts/messageModifiers/MessageReceipts.ts index d8b3d7f559..85c6e031a7 100644 --- a/ts/messageModifiers/MessageReceipts.ts +++ b/ts/messageModifiers/MessageReceipts.ts @@ -23,7 +23,7 @@ import { import { DataReader, DataWriter } from '../sql/Client.js'; import type { DeleteSentProtoRecipientOptionsType } from '../sql/Interface.js'; import { createLogger } from '../logging/log.js'; -import { getSourceServiceId } from '../messages/helpers.js'; +import { getSourceServiceId } from '../messages/sources.js'; import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp.js'; import { getMessageIdForLogging } from '../util/idForLogging.js'; import { getPropForTimestamp } from '../util/editHelpers.js'; @@ -34,7 +34,8 @@ import { import { drop } from '../util/drop.js'; import { getMessageById } from '../messages/getMessageById.js'; import { MessageModel } from '../models/messages.js'; -import { areStoryViewReceiptsEnabled } from '../types/Stories.js'; +import { areStoryViewReceiptsEnabled } from '../util/Settings.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { groupBy } = lodash; @@ -395,12 +396,12 @@ const shouldDropReceipt = ( case messageReceiptTypeSchema.Enum.Delivery: return false; case messageReceiptTypeSchema.Enum.Read: - return !window.storage.get('read-receipt-setting'); + return !itemStorage.get('read-receipt-setting'); case messageReceiptTypeSchema.Enum.View: if (isStory(message)) { return !areStoryViewReceiptsEnabled(); } - return !window.storage.get('read-receipt-setting'); + return !itemStorage.get('read-receipt-setting'); default: throw missingCaseError(type); } @@ -417,7 +418,7 @@ export async function forMessage( message )})`; - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const sourceServiceId = getSourceServiceId(message); if (ourAci !== sourceServiceId) { return []; diff --git a/ts/messageModifiers/Polls.ts b/ts/messageModifiers/Polls.ts index 80c4cb1a1a..a0e7a78161 100644 --- a/ts/messageModifiers/Polls.ts +++ b/ts/messageModifiers/Polls.ts @@ -11,7 +11,8 @@ import { MessageModel } from '../models/messages.js'; import { DataReader } from '../sql/Client.js'; import * as Errors from '../types/errors.js'; import { createLogger } from '../logging/log.js'; -import { getAuthor, isIncoming, isOutgoing } from '../messages/helpers.js'; +import { isIncoming, isOutgoing } from '../messages/helpers.js'; +import { getAuthor } from '../messages/sources.js'; import { isSent } from '../messages/MessageSendState.js'; import { getPropForTimestamp } from '../util/editHelpers.js'; diff --git a/ts/messageModifiers/Reactions.ts b/ts/messageModifiers/Reactions.ts index 5ec905fe11..36a87c5f8b 100644 --- a/ts/messageModifiers/Reactions.ts +++ b/ts/messageModifiers/Reactions.ts @@ -15,12 +15,12 @@ import { DataReader, DataWriter } from '../sql/Client.js'; import * as Errors from '../types/errors.js'; import { createLogger } from '../logging/log.js'; import { - getAuthor, isIncoming, isIncomingStory, isOutgoing, isOutgoingStory, } from '../messages/helpers.js'; +import { getAuthor } from '../messages/sources.js'; import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet.js'; import { isDirectConversation, isMe } from '../util/whatTypeOfConversation.js'; import { @@ -45,6 +45,7 @@ import { conversationQueueJobEnum, } from '../jobs/conversationJobQueue.js'; import { maybeNotify } from '../messages/maybeNotify.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { maxBy } = lodash; @@ -77,7 +78,7 @@ function remove(reaction: ReactionAttributesType): void { export function findReactionsForMessage( message: ReadonlyMessageAttributesType ): Array { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const matchingReactions = Array.from(reactionCache.values()).filter( reaction => { return isMessageAMatchForReaction({ @@ -106,7 +107,7 @@ async function findMessageForReaction({ logId: string; }): Promise { const messages = await DataReader.getMessagesBySentAt(targetTimestamp); - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const matchingMessages = messages.filter(message => isMessageAMatchForReaction({ diff --git a/ts/messageModifiers/ReadSyncs.ts b/ts/messageModifiers/ReadSyncs.ts index c26387dbe5..d8a9d15a30 100644 --- a/ts/messageModifiers/ReadSyncs.ts +++ b/ts/messageModifiers/ReadSyncs.ts @@ -19,6 +19,7 @@ import { isAciString } from '../util/isAciString.js'; import { DataReader, DataWriter } from '../sql/Client.js'; import { markRead } from '../services/MessageUpdater.js'; import { MessageModel } from '../models/messages.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('ReadSyncs'); @@ -62,7 +63,7 @@ async function maybeItIsAReactionReadSync( if ( !readReaction || - readReaction?.targetAuthorAci !== window.storage.user.getCheckedAci() + readReaction?.targetAuthorAci !== itemStorage.user.getCheckedAci() ) { log.info( `${logId} not found:`, diff --git a/ts/messages/copyQuote.ts b/ts/messages/copyQuote.ts index 74f53baec1..e6ad57ff23 100644 --- a/ts/messages/copyQuote.ts +++ b/ts/messages/copyQuote.ts @@ -11,7 +11,8 @@ import type { ProcessedQuote } from '../textsecure/Types.js'; import { IMAGE_JPEG } from '../types/MIME.js'; import { strictAssert } from '../util/assert.js'; import { getQuoteBodyText } from '../util/getQuoteBodyText.js'; -import { isQuoteAMatch, messageHasPaymentEvent } from './helpers.js'; +import { isQuoteAMatch } from './quotes.js'; +import { messageHasPaymentEvent } from './payments.js'; import * as Errors from '../types/errors.js'; import type { MessageModel } from '../models/messages.js'; import { isDownloadable } from '../util/Attachment.js'; diff --git a/ts/messages/handleDataMessage.ts b/ts/messages/handleDataMessage.ts index b041614191..5dd84110e6 100644 --- a/ts/messages/handleDataMessage.ts +++ b/ts/messages/handleDataMessage.ts @@ -8,7 +8,9 @@ import { createLogger } from '../logging/log.js'; import * as Errors from '../types/errors.js'; import * as LinkPreview from '../types/LinkPreview.js'; -import { getAuthor, isStory, messageHasPaymentEvent } from './helpers.js'; +import { isStory } from './helpers.js'; +import { getAuthor } from './sources.js'; +import { messageHasPaymentEvent } from './payments.js'; import { getMessageIdForLogging } from '../util/idForLogging.js'; import { getSenderIdentifier } from '../util/getSenderIdentifier.js'; import { isNormalNumber } from '../util/isNormalNumber.js'; @@ -69,6 +71,7 @@ import type { import type { ServiceIdString } from '../types/ServiceId.js'; import type { LinkPreviewType } from '../types/message/LinkPreviews.js'; import { getCachedSubscriptionConfiguration } from '../util/subscriptionConfiguration.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { isNumber } = lodash; @@ -294,7 +297,7 @@ export async function handleDataMessage( } } - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const sender = window.ConversationController.lookupOrCreate({ e164: source, @@ -305,9 +308,9 @@ export async function handleDataMessage( // Drop if from blocked user. Only GroupV2 messages should need to be dropped here. const isBlocked = - (source && window.storage.blocked.isBlocked(source)) || + (source && itemStorage.blocked.isBlocked(source)) || (sourceServiceId && - window.storage.blocked.isServiceIdBlocked(sourceServiceId)); + itemStorage.blocked.isServiceIdBlocked(sourceServiceId)); if (isBlocked) { log.info( `${idLog}: Dropping message from blocked sender. hasGroupV2Prop: ${hasGroupV2Prop}` @@ -424,7 +427,7 @@ export async function handleDataMessage( const sendState = sendStateByConversationId[sender.id]; const storyQuoteIsFromSelf = - candidateQuote.sourceServiceId === window.storage.user.getCheckedAci(); + candidateQuote.sourceServiceId === itemStorage.user.getCheckedAci(); if (!storyQuoteIsFromSelf) { return true; @@ -563,7 +566,7 @@ export async function handleDataMessage( ); } - const ourPni = window.textsecure.storage.user.getCheckedPni(); + const ourPni = itemStorage.user.getCheckedPni(); const ourServiceIds: Set = new Set([ourAci, ourPni]); // eslint-disable-next-line no-param-reassign @@ -726,8 +729,8 @@ export async function handleDataMessage( if (initialMessage.profileKey) { const { profileKey } = initialMessage; if ( - source === window.textsecure.storage.user.getNumber() || - sourceServiceId === window.textsecure.storage.user.getAci() + source === itemStorage.user.getNumber() || + sourceServiceId === itemStorage.user.getAci() ) { conversation.set({ profileSharing: true }); } else if (isDirectConversation(conversation.attributes)) { diff --git a/ts/messages/helpers.ts b/ts/messages/helpers.ts index fdad29da85..ddbd764252 100644 --- a/ts/messages/helpers.ts +++ b/ts/messages/helpers.ts @@ -3,22 +3,12 @@ import type { ReadonlyDeep } from 'type-fest'; -import { createLogger } from '../logging/log.js'; -import type { ConversationModel } from '../models/conversations.js'; import type { CustomError, ReadonlyMessageAttributesType, QuotedAttachmentType, - QuotedMessageType, } from '../model-types.d.ts'; -import type { AciString, ServiceIdString } from '../types/ServiceId.js'; -import { PaymentEventKind } from '../types/Payment.js'; -import type { AnyPaymentEvent } from '../types/Payment.js'; -import type { LocalizerType } from '../types/Util.js'; -import { missingCaseError } from '../util/missingCaseError.js'; -import { isDownloaded } from '../util/Attachment.js'; - -const log = createLogger('helpers'); +import type { AciString } from '../types/ServiceId.js'; export function isIncoming( message: Pick @@ -59,107 +49,8 @@ export function isIncomingStory( return isStory(message) && !isFromUs(message, ourAci); } -export type MessageAttributesWithPaymentEvent = ReadonlyMessageAttributesType & - ReadonlyDeep<{ - payment: AnyPaymentEvent; - }>; - -export function messageHasPaymentEvent( - message: ReadonlyMessageAttributesType -): message is MessageAttributesWithPaymentEvent { - return message.payment != null; -} - -export function getPaymentEventNotificationText( - payment: ReadonlyDeep, - senderTitle: string, - conversationTitle: string | null, - senderIsMe: boolean, - i18n: LocalizerType -): string { - if (payment.kind === PaymentEventKind.Notification) { - return i18n('icu:payment-event-notification-label'); - } - return getPaymentEventDescription( - payment, - senderTitle, - conversationTitle, - senderIsMe, - i18n - ); -} - -export function getPaymentEventDescription( - payment: ReadonlyDeep, - senderTitle: string, - conversationTitle: string | null, - senderIsMe: boolean, - i18n: LocalizerType -): string { - const { kind } = payment; - if (kind === PaymentEventKind.Notification) { - if (senderIsMe) { - if (conversationTitle != null) { - return i18n('icu:payment-event-notification-message-you-label', { - receiver: conversationTitle, - }); - } - return i18n( - 'icu:payment-event-notification-message-you-label-without-receiver' - ); - } - return i18n('icu:payment-event-notification-message-label', { - sender: senderTitle, - }); - } - if (kind === PaymentEventKind.ActivationRequest) { - if (senderIsMe) { - if (conversationTitle != null) { - return i18n('icu:payment-event-activation-request-you-label', { - receiver: conversationTitle, - }); - } - return i18n( - 'icu:payment-event-activation-request-you-label-without-receiver' - ); - } - return i18n('icu:payment-event-activation-request-label', { - sender: senderTitle, - }); - } - if (kind === PaymentEventKind.Activation) { - if (senderIsMe) { - return i18n('icu:payment-event-activated-you-label'); - } - return i18n('icu:payment-event-activated-label', { - sender: senderTitle, - }); - } - throw missingCaseError(kind); -} - -export function isQuoteAMatch( - message: ReadonlyMessageAttributesType | null | undefined, - conversationId: string, - quote: ReadonlyDeep> -): message is ReadonlyMessageAttributesType { - if (!message) { - return false; - } - - const { authorAci, id } = quote; - - const isSameTimestamp = - message.sent_at === id || - message.editHistory?.some(({ timestamp }) => timestamp === id) || - false; - - return ( - isSameTimestamp && - message.conversationId === conversationId && - getSourceServiceId(message) === authorAci - ); -} +export const isCustomError = (e: unknown): e is CustomError => + e instanceof Error; export const shouldTryToCopyFromQuotedMessage = ({ referencedMessageNotFound, @@ -179,75 +70,9 @@ export const shouldTryToCopyFromQuotedMessage = ({ } // If we already have this file, no need to copy anything - if (isDownloaded(quoteAttachment.thumbnail)) { + if (quoteAttachment.thumbnail.path) { return false; } return true; }; - -export function getAuthorId( - message: Pick< - ReadonlyMessageAttributesType, - 'type' | 'source' | 'sourceServiceId' - > -): string | undefined { - const source = getSource(message); - const sourceServiceId = getSourceServiceId(message); - - if (!source && !sourceServiceId) { - return window.ConversationController.getOurConversationId(); - } - - const conversation = window.ConversationController.lookupOrCreate({ - e164: source, - serviceId: sourceServiceId, - reason: 'helpers.getAuthorId', - }); - return conversation?.id; -} - -export function getAuthor( - message: ReadonlyMessageAttributesType -): ConversationModel | undefined { - const id = getAuthorId(message); - return window.ConversationController.get(id); -} - -export function getSource( - message: Pick -): string | undefined { - if (isIncoming(message) || isStory(message)) { - return message.source; - } - if (!isOutgoing(message)) { - log.warn('Message.getSource: Called for non-incoming/non-outgoing message'); - } - - return window.textsecure.storage.user.getNumber(); -} - -export function getSourceDevice( - message: Pick -): string | number | undefined { - const { sourceDevice } = message; - - if (isIncoming(message) || isStory(message)) { - return sourceDevice; - } - - return sourceDevice || window.textsecure.storage.user.getDeviceId(); -} - -export function getSourceServiceId( - message: Pick -): ServiceIdString | undefined { - if (isIncoming(message) || isStory(message)) { - return message.sourceServiceId; - } - - return window.textsecure.storage.user.getAci(); -} - -export const isCustomError = (e: unknown): e is CustomError => - e instanceof Error; diff --git a/ts/messages/maybeNotify.ts b/ts/messages/maybeNotify.ts index e0c5cad3de..5e16e73981 100644 --- a/ts/messages/maybeNotify.ts +++ b/ts/messages/maybeNotify.ts @@ -3,7 +3,8 @@ import { createLogger } from '../logging/log.js'; -import { getAuthor, isIncoming, isOutgoing } from './helpers.js'; +import { isIncoming, isOutgoing } from './helpers.js'; +import { getAuthor } from './sources.js'; import type { ConversationModel } from '../models/conversations.js'; import { getActiveProfile } from '../state/selectors/notificationProfiles.js'; diff --git a/ts/messages/migrateMessageData.ts b/ts/messages/migrateMessageData.ts index e5ee85fc48..78697105aa 100644 --- a/ts/messages/migrateMessageData.ts +++ b/ts/messages/migrateMessageData.ts @@ -14,6 +14,7 @@ import * as Errors from '../types/errors.js'; import { DataReader, DataWriter } from '../sql/Client.js'; import { postSaveUpdates } from '../util/cleanup.js'; import { createLogger } from '../logging/log.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { isFunction, isNumber } = lodash; @@ -117,7 +118,7 @@ export async function _migrateMessageData({ const saveStartTime = Date.now(); - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const { failedIndices: failedToSaveIndices } = await saveMessagesIndividually( upgradedMessages, { diff --git a/ts/messages/payments.ts b/ts/messages/payments.ts new file mode 100644 index 0000000000..446d2cf8b1 --- /dev/null +++ b/ts/messages/payments.ts @@ -0,0 +1,89 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { ReadonlyDeep } from 'type-fest'; + +import { PaymentEventKind } from '../types/Payment.js'; +import type { AnyPaymentEvent } from '../types/Payment.js'; +import type { LocalizerType } from '../types/Util.js'; +import type { ReadonlyMessageAttributesType } from '../model-types.d.ts'; +import { missingCaseError } from '../util/missingCaseError.js'; + +export type MessageAttributesWithPaymentEvent = ReadonlyMessageAttributesType & + ReadonlyDeep<{ + payment: AnyPaymentEvent; + }>; + +export function messageHasPaymentEvent( + message: ReadonlyMessageAttributesType +): message is MessageAttributesWithPaymentEvent { + return message.payment != null; +} + +export function getPaymentEventNotificationText( + payment: ReadonlyDeep, + senderTitle: string, + conversationTitle: string | null, + senderIsMe: boolean, + i18n: LocalizerType +): string { + if (payment.kind === PaymentEventKind.Notification) { + return i18n('icu:payment-event-notification-label'); + } + return getPaymentEventDescription( + payment, + senderTitle, + conversationTitle, + senderIsMe, + i18n + ); +} + +export function getPaymentEventDescription( + payment: ReadonlyDeep, + senderTitle: string, + conversationTitle: string | null, + senderIsMe: boolean, + i18n: LocalizerType +): string { + const { kind } = payment; + if (kind === PaymentEventKind.Notification) { + if (senderIsMe) { + if (conversationTitle != null) { + return i18n('icu:payment-event-notification-message-you-label', { + receiver: conversationTitle, + }); + } + return i18n( + 'icu:payment-event-notification-message-you-label-without-receiver' + ); + } + return i18n('icu:payment-event-notification-message-label', { + sender: senderTitle, + }); + } + if (kind === PaymentEventKind.ActivationRequest) { + if (senderIsMe) { + if (conversationTitle != null) { + return i18n('icu:payment-event-activation-request-you-label', { + receiver: conversationTitle, + }); + } + return i18n( + 'icu:payment-event-activation-request-you-label-without-receiver' + ); + } + return i18n('icu:payment-event-activation-request-label', { + sender: senderTitle, + }); + } + if (kind === PaymentEventKind.Activation) { + if (senderIsMe) { + return i18n('icu:payment-event-activated-you-label'); + } + return i18n('icu:payment-event-activated-label', { + sender: senderTitle, + }); + } + throw missingCaseError(kind); +} diff --git a/ts/messages/quotes.ts b/ts/messages/quotes.ts new file mode 100644 index 0000000000..6d8f5ab996 --- /dev/null +++ b/ts/messages/quotes.ts @@ -0,0 +1,33 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { ReadonlyDeep } from 'type-fest'; + +import type { + ReadonlyMessageAttributesType, + QuotedMessageType, +} from '../model-types.d.ts'; +import { getSourceServiceId } from './sources.js'; + +export function isQuoteAMatch( + message: ReadonlyMessageAttributesType | null | undefined, + conversationId: string, + quote: ReadonlyDeep> +): message is ReadonlyMessageAttributesType { + if (!message) { + return false; + } + + const { authorAci, id } = quote; + + const isSameTimestamp = + message.sent_at === id || + message.editHistory?.some(({ timestamp }) => timestamp === id) || + false; + + return ( + isSameTimestamp && + message.conversationId === conversationId && + getSourceServiceId(message) === authorAci + ); +} diff --git a/ts/messages/send.ts b/ts/messages/send.ts index abf17a4392..2129ff1576 100644 --- a/ts/messages/send.ts +++ b/ts/messages/send.ts @@ -29,6 +29,7 @@ import { import type { CustomError, MessageAttributesType } from '../model-types.d.ts'; import type { CallbackResultType } from '../textsecure/Types.d.ts'; +import { messageSender } from '../textsecure/SendMessage.js'; import type { MessageModel } from '../models/messages.js'; import type { ServiceIdString } from '../types/ServiceId.js'; import type { SendStateByConversationId } from './MessageSendState.js'; @@ -343,11 +344,6 @@ export async function sendSyncMessage( return; } - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error('sendSyncMessage: messaging not available!'); - } - // eslint-disable-next-line no-param-reassign message.syncPromise = message.syncPromise || Promise.resolve(); const next = async () => { @@ -410,7 +406,7 @@ export async function sendSyncMessage( }; return handleMessageSend( - messaging.sendSyncMessage({ + messageSender.sendSyncMessage({ ...encodedContent, timestamp: targetTimestamp, destinationE164: conv.get('e164'), diff --git a/ts/messages/sources.ts b/ts/messages/sources.ts new file mode 100644 index 0000000000..f30ccd6d9d --- /dev/null +++ b/ts/messages/sources.ts @@ -0,0 +1,74 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { ReadonlyMessageAttributesType } from '../model-types.d.ts'; +import { createLogger } from '../logging/log.js'; +import { itemStorage } from '../textsecure/Storage.js'; +import type { ConversationModel } from '../models/conversations.js'; +import type { ServiceIdString } from '../types/ServiceId.js'; +import { isIncoming, isOutgoing, isStory } from './helpers.js'; + +const log = createLogger('messages/sources'); + +export function getSource( + message: Pick +): string | undefined { + if (isIncoming(message) || isStory(message)) { + return message.source; + } + if (!isOutgoing(message)) { + log.warn('Message.getSource: Called for non-incoming/non-outgoing message'); + } + + return itemStorage.user.getNumber(); +} + +export function getSourceDevice( + message: Pick +): string | number | undefined { + const { sourceDevice } = message; + + if (isIncoming(message) || isStory(message)) { + return sourceDevice; + } + + return sourceDevice || itemStorage.user.getDeviceId(); +} + +export function getSourceServiceId( + message: Pick +): ServiceIdString | undefined { + if (isIncoming(message) || isStory(message)) { + return message.sourceServiceId; + } + + return itemStorage.user.getAci(); +} + +export function getAuthorId( + message: Pick< + ReadonlyMessageAttributesType, + 'type' | 'source' | 'sourceServiceId' + > +): string | undefined { + const source = getSource(message); + const sourceServiceId = getSourceServiceId(message); + + if (!source && !sourceServiceId) { + return window.ConversationController.getOurConversationId(); + } + + const conversation = window.ConversationController.lookupOrCreate({ + e164: source, + serviceId: sourceServiceId, + reason: 'helpers.getAuthorId', + }); + return conversation?.id; +} + +export function getAuthor( + message: ReadonlyMessageAttributesType +): ConversationModel | undefined { + const id = getAuthorId(message); + return window.ConversationController.get(id); +} diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index bbb87cd4b9..91d7982a7c 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -46,9 +46,17 @@ import type { import * as Stickers from '../types/Stickers.js'; import { StorySendMode } from '../types/Stories.js'; import type { EmbeddedContactWithHydratedAvatar } from '../types/EmbeddedContact.js'; -import type { GroupV2InfoType } from '../textsecure/SendMessage.js'; +import { + type GroupV2InfoType, + messageSender, +} from '../textsecure/SendMessage.js'; +import { + getAvatar as doGetAvatar, + cdsLookup, + checkAccountExistence, +} from '../textsecure/WebAPI.js'; import createTaskWithTimeout from '../textsecure/TaskWithTimeout.js'; -import MessageSender from '../textsecure/SendMessage.js'; +import { MessageSender } from '../textsecure/SendMessage.js'; import type { CallbackResultType, PniSignatureMessageType, @@ -223,11 +231,12 @@ import { waitThenRespondToGroupV2Migration, } from '../groups.js'; import { safeSetTimeout } from '../util/timeout.js'; -import { getTypingIndicatorSetting } from '../types/Util.js'; +import { getTypingIndicatorSetting } from '../util/Settings.js'; import { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer.js'; import { maybeNotify } from '../messages/maybeNotify.js'; import { missingCaseError } from '../util/missingCaseError.js'; import * as Message from '../types/Message2.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { compact, isNumber, throttle, debounce } = lodash; @@ -570,7 +579,7 @@ export class ConversationModel { const idLog = this.idForLogging(); const us = window.ConversationController.getOurConversationOrThrow(); - const serviceId = window.storage.user.getCheckedServiceId(serviceIdKind); + const serviceId = itemStorage.user.getCheckedServiceId(serviceIdKind); // This user's pending state may have changed in the time between the user's // button press and when we get here. It's especially important to check here @@ -630,7 +639,7 @@ export class ConversationModel { return undefined; } - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); return buildDeletePendingAdminApprovalMemberChange({ group: this.attributes, @@ -750,7 +759,7 @@ export class ConversationModel { return undefined; } - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); return buildDeleteMemberChange({ group: this.attributes, @@ -945,19 +954,19 @@ export class ConversationModel { const serviceId = this.getServiceId(); if (serviceId && isAciString(serviceId)) { - drop(window.storage.blocked.addBlockedServiceId(serviceId)); + drop(itemStorage.blocked.addBlockedServiceId(serviceId)); blocked = true; } const e164 = this.get('e164'); if (e164) { - drop(window.storage.blocked.addBlockedNumber(e164)); + drop(itemStorage.blocked.addBlockedNumber(e164)); blocked = true; } const groupId = this.get('groupId'); if (groupId) { - drop(window.storage.blocked.addBlockedGroup(groupId)); + drop(itemStorage.blocked.addBlockedGroup(groupId)); blocked = true; } @@ -976,19 +985,19 @@ export class ConversationModel { const serviceId = this.getServiceId(); if (serviceId && isAciString(serviceId)) { - drop(window.storage.blocked.removeBlockedServiceId(serviceId)); + drop(itemStorage.blocked.removeBlockedServiceId(serviceId)); unblocked = true; } const e164 = this.get('e164'); if (e164) { - drop(window.storage.blocked.removeBlockedNumber(e164)); + drop(itemStorage.blocked.removeBlockedNumber(e164)); unblocked = true; } const groupId = this.get('groupId'); if (groupId) { - drop(window.storage.blocked.removeBlockedGroup(groupId)); + drop(itemStorage.blocked.removeBlockedGroup(groupId)); unblocked = true; } @@ -1219,10 +1228,6 @@ export class ConversationModel { } async fetchSMSOnlyUUID(): Promise { - const { server } = window.textsecure; - if (!server) { - return; - } if (!this.isSMSOnly()) { return; } @@ -1239,7 +1244,10 @@ export class ConversationModel { await updateConversationsWithUuidLookup({ conversationController: window.ConversationController, conversations: [this], - server, + server: { + cdsLookup, + checkAccountExistence, + }, }); } finally { // No redux update here @@ -1341,12 +1349,6 @@ export class ConversationModel { } async sendTypingMessage(isTyping: boolean): Promise { - const { messaging } = window.textsecure; - - if (!messaging) { - return; - } - // We don't send typing messages to our other devices if (isMe(this.attributes)) { return; @@ -1419,7 +1421,7 @@ export class ConversationModel { `sendTypingMessage(${this.idForLogging()}): sending ${content.isTyping}` ); - const contentMessage = messaging.getTypingContentMessage(content); + const contentMessage = messageSender.getTypingContentMessage(content); const sendOptions = { ...(await getSendOptions(this.attributes)), @@ -1427,7 +1429,7 @@ export class ConversationModel { }; if (isDirectConversation(this.attributes)) { await handleMessageSend( - messaging.sendMessageProtoAndWait({ + messageSender.sendMessageProtoAndWait({ contentHint: ContentHint.Implicit, groupId: undefined, options: sendOptions, @@ -2574,8 +2576,8 @@ export class ConversationModel { } if (isLocalAction) { - const ourAci = window.textsecure.storage.user.getCheckedAci(); - const ourPni = window.textsecure.storage.user.getPni(); + const ourAci = itemStorage.user.getCheckedAci(); + const ourPni = itemStorage.user.getPni(); const ourConversation = window.ConversationController.getOurConversationOrThrow(); @@ -2657,7 +2659,7 @@ export class ConversationModel { inviteLinkPassword: string; approvalRequired: boolean; }): Promise { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const ourConversation = window.ConversationController.getOurConversationOrThrow(); try { @@ -2713,7 +2715,7 @@ export class ConversationModel { } async cancelJoinRequest(): Promise { - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const inviteLinkPassword = this.get('groupInviteLinkPassword'); if (!inviteLinkPassword) { @@ -2735,8 +2737,8 @@ export class ConversationModel { return; } - const ourAci = window.textsecure.storage.user.getCheckedAci(); - const ourPni = window.textsecure.storage.user.getPni(); + const ourAci = itemStorage.user.getCheckedAci(); + const ourPni = itemStorage.user.getPni(); if (this.isMemberPending(ourAci)) { await this.modifyGroupV2({ @@ -3633,9 +3635,8 @@ export class ConversationModel { 'Change number notification without service id' ); - const { storage } = window.textsecure; if ( - storage.user.getOurServiceIdKind(sourceServiceId) !== + itemStorage.user.getOurServiceIdKind(sourceServiceId) !== ServiceIdKind.Unknown ) { log.info( @@ -4861,7 +4862,7 @@ export class ConversationModel { return undefined; } - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const theirAci = this.getAci(); if (!theirAci) { return undefined; @@ -4972,9 +4973,9 @@ export class ConversationModel { const { avatarUrl, decryptionKey, forceFetch } = options; if (isMe(this.attributes)) { if (avatarUrl) { - await window.storage.put('avatarUrl', avatarUrl); + await itemStorage.put('avatarUrl', avatarUrl); } else { - await window.storage.remove('avatarUrl'); + await itemStorage.remove('avatarUrl'); } } @@ -4983,17 +4984,12 @@ export class ConversationModel { return; } - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error('setProfileAvatar: Cannot fetch avatar when offline!'); - } - if (!this.getAccepted({ ignoreEmptyConvo: true }) && !forceFetch) { this.set({ remoteAvatarUrl: avatarUrl }); return; } - const avatar = await messaging.getAvatar(avatarUrl); + const avatar = await doGetAvatar(avatarUrl); // If decryptionKey isn't provided, use the one from the model const modelProfileKey = this.get('profileKey'); @@ -5365,7 +5361,7 @@ export class ConversationModel { areWeAMember(): boolean { return ( - !this.get('left') && this.hasMember(window.storage.user.getCheckedAci()) + !this.get('left') && this.hasMember(itemStorage.user.getCheckedAci()) ); } @@ -5693,7 +5689,7 @@ export class ConversationModel { log.info('pinning', this.idForLogging()); const pinnedConversationIds = new Set( - window.storage.get('pinnedConversationIds', new Array()) + itemStorage.get('pinnedConversationIds', new Array()) ); pinnedConversationIds.add(this.id); @@ -5716,7 +5712,7 @@ export class ConversationModel { log.info('un-pinning', this.idForLogging()); const pinnedConversationIds = new Set( - window.storage.get('pinnedConversationIds', new Array()) + itemStorage.get('pinnedConversationIds', new Array()) ); pinnedConversationIds.delete(this.id); @@ -5728,7 +5724,7 @@ export class ConversationModel { } writePinnedConversations(pinnedConversationIds: Array): void { - drop(window.storage.put('pinnedConversationIds', pinnedConversationIds)); + drop(itemStorage.put('pinnedConversationIds', pinnedConversationIds)); const myId = window.ConversationController.getOurConversationId(); const me = window.ConversationController.get(myId); diff --git a/ts/quill/auto-substitute-ascii-emojis/index.tsx b/ts/quill/auto-substitute-ascii-emojis/index.tsx index c167e57474..ef40fe6366 100644 --- a/ts/quill/auto-substitute-ascii-emojis/index.tsx +++ b/ts/quill/auto-substitute-ascii-emojis/index.tsx @@ -50,6 +50,8 @@ function buildRegexp(obj: EmojiShortcutMap): RegExp { const EMOJI_REGEXP = buildRegexp(emojiShortcutMap); +let isEnabled = true; + export class AutoSubstituteAsciiEmojis { options: AutoSubstituteAsciiEmojisOptions; @@ -72,8 +74,12 @@ export class AutoSubstituteAsciiEmojis { }); } + static enable(value: boolean): void { + isEnabled = value; + } + onTextChange(): void { - if (!window.storage.get('autoConvertEmoji', true)) { + if (!isEnabled) { return; } diff --git a/ts/reactions/enqueueReactionForSend.ts b/ts/reactions/enqueueReactionForSend.ts index 76893f9fe4..1c876cfe9f 100644 --- a/ts/reactions/enqueueReactionForSend.ts +++ b/ts/reactions/enqueueReactionForSend.ts @@ -12,7 +12,8 @@ import { } from '../messageModifiers/Reactions.js'; import { ReactionSource } from './ReactionSource.js'; import { getMessageById } from '../messages/getMessageById.js'; -import { getSourceServiceId, isStory } from '../messages/helpers.js'; +import { getSourceServiceId } from '../messages/sources.js'; +import { isStory } from '../messages/helpers.js'; import { strictAssert } from '../util/assert.js'; import { isDirectConversation } from '../util/whatTypeOfConversation.js'; import { incrementMessageCounter } from '../util/incrementMessageCounter.js'; diff --git a/ts/services/LinkPreview.ts b/ts/services/LinkPreview.ts index c210172d82..f983f661b5 100644 --- a/ts/services/LinkPreview.ts +++ b/ts/services/LinkPreview.ts @@ -19,6 +19,7 @@ import type { MIMEType } from '../types/MIME.js'; import * as Bytes from '../Bytes.js'; import { sha256 } from '../Crypto.js'; import * as LinkPreview from '../types/LinkPreview.js'; +import { getLinkPreviewSetting } from '../util/Settings.js'; import * as Stickers from '../types/Stickers.js'; import * as VisualAttachment from '../types/VisualAttachment.js'; import { createLogger } from '../logging/log.js'; @@ -41,6 +42,11 @@ import { drop } from '../util/drop.js'; import { calling } from './calling.js'; import { getKeyAndEpochFromCallLink } from '../util/callLinks.js'; import { getRoomIdFromCallLink } from '../util/callLinksRingrtc.js'; +import { + fetchLinkPreviewImage, + fetchLinkPreviewMetadata, +} from '../textsecure/WebAPI.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { debounce, omit } = lodash; @@ -75,13 +81,7 @@ function _maybeGrabLinkPreview( ): void { // Don't generate link previews if user has turned them off. When posting a // story we should return minimal (url-only) link previews. - if (!LinkPreview.getLinkPreviewSetting() && mode === 'conversation') { - return; - } - - // Do nothing if we're offline - const { messaging } = window.textsecure; - if (!messaging) { + if (!getLinkPreviewSetting() && mode === 'conversation') { return; } @@ -114,7 +114,7 @@ function _maybeGrabLinkPreview( drop( addLinkPreview(link, source, { conversationId, - disableFetch: !LinkPreview.getLinkPreviewSetting(), + disableFetch: !getLinkPreviewSetting(), }) ); } @@ -256,7 +256,7 @@ export function getLinkPreviewForSend( message: string ): Array { // Don't generate link previews if user has turned them off - if (!window.storage.get('linkPreviews', false)) { + if (!itemStorage.get('linkPreviews', false)) { return []; } @@ -306,12 +306,6 @@ async function getPreview( url: string, abortSignal: Readonly ): Promise { - const { messaging } = window.textsecure; - - if (!messaging) { - throw new Error('messaging is not available!'); - } - if (LinkPreview.isStickerPack(url)) { return getStickerPackPreview(url, abortSignal); } @@ -327,10 +321,7 @@ async function getPreview( return null; } - const linkPreviewMetadata = await messaging.fetchLinkPreviewMetadata( - url, - abortSignal - ); + const linkPreviewMetadata = await fetchLinkPreviewMetadata(url, abortSignal); if (!linkPreviewMetadata || abortSignal.aborted) { log.warn('aborted'); return null; @@ -345,10 +336,7 @@ async function getPreview( fetchedImage = null; } else { try { - const fullSizeImage = await messaging.fetchLinkPreviewImage( - image, - abortSignal - ); + const fullSizeImage = await fetchLinkPreviewImage(image, abortSignal); if (abortSignal.aborted) { return null; } diff --git a/ts/services/MessageCache.ts b/ts/services/MessageCache.ts index 38d228c969..4bec429661 100644 --- a/ts/services/MessageCache.ts +++ b/ts/services/MessageCache.ts @@ -17,6 +17,7 @@ import { postSaveUpdates } from '../util/cleanup.js'; import type { MessageAttributesType } from '../model-types.d.ts'; import type { SendStateByConversationId } from '../messages/MessageSendState.js'; import type { StoredJob } from '../jobs/types.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { throttle } = lodash; @@ -48,7 +49,7 @@ export class MessageCache { message instanceof MessageModel ? message.attributes : message; return DataWriter.saveMessage(attributes, { - ourAci: window.textsecure.storage.user.getCheckedAci(), + ourAci: itemStorage.user.getCheckedAci(), postSaveUpdates, ...options, }); diff --git a/ts/services/MessageUpdater.ts b/ts/services/MessageUpdater.ts index b46e2b48c6..79a079f154 100644 --- a/ts/services/MessageUpdater.ts +++ b/ts/services/MessageUpdater.ts @@ -12,7 +12,7 @@ import { createLogger } from '../logging/log.js'; import { isValidTapToView } from '../util/isValidTapToView.js'; import { getMessageIdForLogging } from '../util/idForLogging.js'; import { eraseMessageContents } from '../util/cleanup.js'; -import { getSource, getSourceServiceId } from '../messages/helpers.js'; +import { getSource, getSourceServiceId } from '../messages/sources.js'; import { isAciString } from '../util/isAciString.js'; import { viewOnceOpenJobQueue } from '../jobs/viewOnceOpenJobQueue.js'; import { drop } from '../util/drop.js'; diff --git a/ts/services/areWeASubscriber.ts b/ts/services/areWeASubscriber.ts index 79b966aae4..26a948bfa2 100644 --- a/ts/services/areWeASubscriber.ts +++ b/ts/services/areWeASubscriber.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { StorageInterface } from '../types/Storage.d.ts'; -import type { WebAPIType } from '../textsecure/WebAPI.js'; +import type { getHasSubscription, isOnline } from '../textsecure/WebAPI.js'; import { LatestQueue } from '../util/LatestQueue.js'; import { waitForOnline } from '../util/waitForOnline.js'; @@ -12,7 +12,10 @@ export class AreWeASubscriberService { update( storage: Pick, - server: Pick + server: { + getHasSubscription: typeof getHasSubscription; + isOnline: typeof isOnline; + } ): void { this.#queue.add(async () => { await new Promise(resolve => storage.onready(resolve)); diff --git a/ts/services/backups/api.ts b/ts/services/backups/api.ts index 5db49b267c..893be02909 100644 --- a/ts/services/backups/api.ts +++ b/ts/services/backups/api.ts @@ -3,9 +3,20 @@ import { type Readable } from 'node:stream'; -import { strictAssert } from '../../util/assert.js'; +import { + backupListMedia, + backupMediaBatch as doBackupMediaBatch, + getBackupFileHeaders, + getBackupInfo, + getBackupMediaUploadForm, + getBackupStream, + getBackupUploadForm, + getEphemeralBackupStream, + getSubscription, + getTransferArchive as doGetTransferArchive, + refreshBackup, +} from '../../textsecure/WebAPI.js'; import type { - WebAPIType, AttachmentUploadFormResponseType, GetBackupInfoResponseType, BackupMediaItemType, @@ -24,6 +35,7 @@ import { uploadFile } from '../../util/uploadAttachment.js'; import { HTTPError } from '../../types/HTTPError.js'; import { createLogger } from '../../logging/log.js'; import { toLogFormat } from '../../types/errors.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const log = createLogger('api'); @@ -55,13 +67,13 @@ export class BackupAPI { this.credentials.getHeadersForToday(type) ) ); - await Promise.all(headers.map(h => this.#server.refreshBackup(h))); + await Promise.all(headers.map(h => refreshBackup(h))); } public async getInfo( credentialType: BackupCredentialType ): Promise { - const backupInfo = await this.#server.getBackupInfo( + const backupInfo = await getBackupInfo( await this.credentials.getHeadersForToday(credentialType) ); this.#cachedBackupInfo.set(credentialType, backupInfo); @@ -88,7 +100,7 @@ export class BackupAPI { } public async upload(filePath: string, fileSize: number): Promise { - const form = await this.#server.getBackupUploadForm( + const form = await getBackupUploadForm( await this.credentials.getHeadersForToday(BackupCredentialType.Messages) ); @@ -112,7 +124,7 @@ export class BackupAPI { BackupCredentialType.Messages ); - return this.#server.getBackupStream({ + return getBackupStream({ cdn, backupDir, backupName, @@ -136,7 +148,7 @@ export class BackupAPI { ); try { const { 'content-length': size, 'last-modified': createdAt } = - await this.#server.getBackupFileHeaders({ + await getBackupFileHeaders({ cdn, backupDir, backupName, @@ -156,7 +168,7 @@ export class BackupAPI { public async getTransferArchive( abortSignal: AbortSignal ): Promise { - return this.#server.getTransferArchive({ + return doGetTransferArchive({ abortSignal, }); } @@ -167,7 +179,7 @@ export class BackupAPI { onProgress, abortSignal, }: EphemeralDownloadOptionsType): Promise { - return this.#server.getEphemeralBackupStream({ + return getEphemeralBackupStream({ cdn: archive.cdn, key: archive.key, downloadOffset, @@ -177,7 +189,7 @@ export class BackupAPI { } public async getMediaUploadForm(): Promise { - return this.#server.getBackupMediaUploadForm( + return getBackupMediaUploadForm( await this.credentials.getHeadersForToday(BackupCredentialType.Media) ); } @@ -185,7 +197,7 @@ export class BackupAPI { public async backupMediaBatch( items: ReadonlyArray ): Promise { - return this.#server.backupMediaBatch({ + return doBackupMediaBatch({ headers: await this.credentials.getHeadersForToday( BackupCredentialType.Media ), @@ -200,7 +212,7 @@ export class BackupAPI { cursor?: string; limit: number; }): Promise { - return this.#server.backupListMedia({ + return backupListMedia({ headers: await this.credentials.getHeadersForToday( BackupCredentialType.Media ), @@ -210,7 +222,7 @@ export class BackupAPI { } public async getSubscriptionInfo(): Promise { - const subscriberId = window.storage.get('backupsSubscriberId'); + const subscriberId = itemStorage.get('backupsSubscriberId'); if (!subscriberId) { log.error('Backups.getSubscriptionInfo: missing subscriberId'); return { status: 'not-found' }; @@ -218,7 +230,7 @@ export class BackupAPI { let subscriptionResponse: SubscriptionResponseType; try { - subscriptionResponse = await this.#server.getSubscription(subscriberId); + subscriptionResponse = await getSubscription(subscriberId); } catch (e) { log.warn( 'Backups.getSubscriptionInfo: error fetching subscription', @@ -269,11 +281,4 @@ export class BackupAPI { public clearCache(): void { this.#cachedBackupInfo.clear(); } - - get #server(): WebAPIType { - const { server } = window.textsecure; - strictAssert(server, 'server not available'); - - return server; - } } diff --git a/ts/services/backups/credentials.ts b/ts/services/backups/credentials.ts index e91526b352..9b2ceec656 100644 --- a/ts/services/backups/credentials.ts +++ b/ts/services/backups/credentials.ts @@ -38,6 +38,12 @@ import type { GetBackupCredentialsResponseType, GetBackupCDNCredentialsResponseType, } from '../../textsecure/WebAPI.js'; +import { + setBackupSignatureKey, + getBackupCDNCredentials, + getBackupCredentials, + setBackupId, +} from '../../textsecure/WebAPI.js'; import { getBackupKey, getBackupMediaRootKey, @@ -49,6 +55,7 @@ import { areRemoteBackupsTurnedOn, canAttemptRemoteBackupDownload, } from '../../util/isBackupEnabled.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { throttle } = lodashFp; @@ -133,21 +140,18 @@ export class BackupCredentials { }; const info = { headers, level: result.level }; - if (window.storage.get(storageKey)) { + if (itemStorage.get(storageKey)) { return info; } log.warn(`uploading signature key (${storageKey})`); - const { server } = window.textsecure; - strictAssert(server, 'server not available'); - - await server.setBackupSignatureKey({ + await setBackupSignatureKey({ headers, backupIdPublicKey: signatureKey.getPublicKey().serialize(), }); - await window.storage.put(storageKey, true); + await itemStorage.put(storageKey, true); return info; } @@ -163,9 +167,6 @@ export class BackupCredentials { cdnNumber: number, credentialType: BackupCredentialType ): Promise { - const { server } = window.textsecure; - strictAssert(server, 'server not available'); - // Backup CDN read credentials are short-lived; we'll just cache them in memory so // that they get invalidated for any reason, we'll fetch new ones on app restart const cachedCredentialsForThisCredentialType = @@ -186,7 +187,7 @@ export class BackupCredentials { const headers = await this.getHeadersForToday(credentialType); const retrievedAtMs = Date.now(); - const newCredentials = await server.getBackupCDNCredentials({ + const newCredentials = await getBackupCDNCredentials({ headers, cdnNumber, }); @@ -201,7 +202,7 @@ export class BackupCredentials { } #scheduleFetch(): void { - const lastFetchAt = window.storage.get( + const lastFetchAt = itemStorage.get( 'backupCombinedCredentialsLastRequestTime', 0 ); @@ -218,7 +219,7 @@ export class BackupCredentials { await this.#fetch(); const now = Date.now(); - await window.storage.put('backupCombinedCredentialsLastRequestTime', now); + await itemStorage.put('backupCombinedCredentialsLastRequestTime', now); this.#fetchBackoff.reset(); this.#scheduleFetch(); @@ -266,12 +267,10 @@ export class BackupCredentials { // And fetch missing credentials const messagesCtx = this.#getAuthContext(BackupCredentialType.Messages); const mediaCtx = this.#getAuthContext(BackupCredentialType.Media); - const { server } = window.textsecure; - strictAssert(server, 'server not available'); let response: GetBackupCredentialsResponseType; try { - response = await server.getBackupCredentials({ + response = await getBackupCredentials({ startDayInMs, endDayInMs, }); @@ -289,13 +288,13 @@ export class BackupCredentials { const mediaRequest = mediaCtx.getRequest(); // Set it - await server.setBackupId({ + await setBackupId({ messagesBackupAuthCredentialRequest: messagesRequest.serialize(), mediaBackupAuthCredentialRequest: mediaRequest.serialize(), }); // And try again! - response = await server.getBackupCredentials({ + response = await getBackupCredentials({ startDayInMs, endDayInMs, }); @@ -411,18 +410,18 @@ export class BackupCredentials { } return BackupAuthCredentialRequestContext.create( key.serialize(), - window.storage.user.getCheckedAci() + itemStorage.user.getCheckedAci() ); } #getFromCache(): ReadonlyArray { - return window.storage.get('backupCombinedCredentials', []); + return itemStorage.get('backupCombinedCredentials', []); } async #updateCache( values: ReadonlyArray ): Promise { - await window.storage.put('backupCombinedCredentials', values); + await itemStorage.put('backupCombinedCredentials', values); } public async getBackupLevel( diff --git a/ts/services/backups/crypto.ts b/ts/services/backups/crypto.ts index d43472e435..bb7dd5c129 100644 --- a/ts/services/backups/crypto.ts +++ b/ts/services/backups/crypto.ts @@ -12,20 +12,21 @@ import { MessageBackupKey } from '@signalapp/libsignal-client/dist/MessageBackup import { strictAssert } from '../../util/assert.js'; import type { AciString } from '../../types/ServiceId.js'; import { toAciObject } from '../../util/ServiceId.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const getMemoizedBackupKey = memoizee((accountEntropyPool: string) => { return AccountEntropyPool.deriveBackupKey(accountEntropyPool); }); export function getBackupKey(): BackupKey { - const accountEntropyPool = window.storage.get('accountEntropyPool'); + const accountEntropyPool = itemStorage.get('accountEntropyPool'); strictAssert(accountEntropyPool, 'Account Entropy Pool not available'); return getMemoizedBackupKey(accountEntropyPool); } export function getBackupMediaRootKey(): BackupKey { - const rootKey = window.storage.get('backupMediaRootKey'); + const rootKey = itemStorage.get('backupMediaRootKey'); strictAssert(rootKey, 'Media root key not available'); return new BackupKey(rootKey); @@ -39,7 +40,7 @@ const getMemoizedBackupSignatureKey = memoizee( export function getBackupSignatureKey(): PrivateKey { const backupKey = getBackupKey(); - const aci = window.storage.user.getCheckedAci(); + const aci = itemStorage.user.getCheckedAci(); return getMemoizedBackupSignatureKey(backupKey, aci); } @@ -51,7 +52,7 @@ const getMemoizedBackupMediaSignatureKey = memoizee( export function getBackupMediaSignatureKey(): PrivateKey { const rootKey = getBackupMediaRootKey(); - const aci = window.storage.user.getCheckedAci(); + const aci = itemStorage.user.getCheckedAci(); return getMemoizedBackupMediaSignatureKey(rootKey, aci); } @@ -74,7 +75,7 @@ export type BackupKeyMaterialType = Readonly<{ export function getKeyMaterial( backupKey = getBackupKey() ): BackupKeyMaterialType { - const aci = window.storage.user.getCheckedAci(); + const aci = itemStorage.user.getCheckedAci(); return getMemoizedKeyMaterial(backupKey, aci); } @@ -125,7 +126,7 @@ export function deriveBackupThumbnailTransitKeyMaterial( } export function getBackupId(): Uint8Array { - const aci = window.storage.user.getCheckedAci(); + const aci = itemStorage.user.getCheckedAci(); return getBackupKey().deriveBackupId(toAciObject(aci)); } diff --git a/ts/services/backups/export.ts b/ts/services/backups/export.ts index e7805a71a4..90b3979e3e 100644 --- a/ts/services/backups/export.ts +++ b/ts/services/backups/export.ts @@ -119,7 +119,7 @@ import { isAciString } from '../../util/isAciString.js'; import { hslToRGBInt } from '../../util/hslToRGB.js'; import type { AboutMe, LocalChatStyle } from './types.js'; import { BackupType } from './types.js'; -import { messageHasPaymentEvent } from '../../messages/helpers.js'; +import { messageHasPaymentEvent } from '../../messages/payments.js'; import { numberToAddressType, numberToPhoneType, @@ -164,9 +164,12 @@ import { calculateLightness } from '../../util/getHSL.js'; import { isSignalServiceId } from '../../util/isSignalConversation.js'; import { isValidE164 } from '../../util/isValidE164.js'; import { toDayOfWeekArray } from '../../types/NotificationProfile.js'; -import { getLinkPreviewSetting } from '../../types/LinkPreview.js'; -import { getTypingIndicatorSetting } from '../../types/Util.js'; +import { + getLinkPreviewSetting, + getTypingIndicatorSetting, +} from '../../util/Settings.js'; import { KIBIBYTE } from '../../types/AttachmentSize.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { isNumber } = lodash; @@ -377,7 +380,7 @@ export class BackupExportStream extends Readable { version: Long.fromNumber(BACKUP_VERSION), backupTimeMs: this.#backupTimeMs, mediaRootBackupKey: getBackupMediaRootKey().serialize(), - firstAppVersion: window.storage.get('restoredBackupFirstAppVersion'), + firstAppVersion: itemStorage.get('restoredBackupFirstAppVersion'), currentAppVersion: `Desktop ${window.getVersion()}`, }).finish() ); @@ -535,7 +538,7 @@ export class BackupExportStream extends Readable { } const pinnedConversationIds = - window.storage.get('pinnedConversationIds') || []; + itemStorage.get('pinnedConversationIds') || []; for (const { attributes } of window.ConversationController.getAll()) { if (isGroupV1(attributes)) { @@ -825,13 +828,9 @@ export class BackupExportStream extends Readable { } async #toAccountData(): Promise { - const { storage } = window; - const me = window.ConversationController.getOurConversationOrThrow(); - const rawPreferredReactionEmoji = window.storage.get( - 'preferredReactionEmoji' - ); + const rawPreferredReactionEmoji = itemStorage.get('preferredReactionEmoji'); let preferredReactionEmoji: Array | undefined; if (canPreferredReactionEmojiBeSynced(rawPreferredReactionEmoji)) { @@ -841,7 +840,7 @@ export class BackupExportStream extends Readable { const PHONE_NUMBER_SHARING_MODE_ENUM = Backups.AccountData.PhoneNumberSharingMode; const rawPhoneNumberSharingMode = parsePhoneNumberSharingMode( - storage.get('phoneNumberSharingMode') + itemStorage.get('phoneNumberSharingMode') ); let phoneNumberSharingMode: Backups.AccountData.PhoneNumberSharingMode; switch (rawPhoneNumberSharingMode) { @@ -856,63 +855,63 @@ export class BackupExportStream extends Readable { throw missingCaseError(rawPhoneNumberSharingMode); } - const usernameLink = storage.get('usernameLink'); + const usernameLink = itemStorage.get('usernameLink'); - const subscriberId = storage.get('subscriberId'); - const currencyCode = storage.get('subscriberCurrencyCode'); + const subscriberId = itemStorage.get('subscriberId'); + const currencyCode = itemStorage.get('subscriberCurrencyCode'); const backupsSubscriberData = generateBackupsSubscriberData(); - const backupTier = storage.get('backupTier'); + const backupTier = itemStorage.get('backupTier'); return { - profileKey: storage.get('profileKey'), + profileKey: itemStorage.get('profileKey'), username: me.get('username') || null, usernameLink: usernameLink ? { ...usernameLink, // Same numeric value, no conversion needed - color: storage.get('usernameLinkColor'), + color: itemStorage.get('usernameLinkColor'), } : null, givenName: me.get('profileName'), familyName: me.get('profileFamilyName'), - avatarUrlPath: storage.get('avatarUrl'), + avatarUrlPath: itemStorage.get('avatarUrl'), backupsSubscriberData, donationSubscriberData: Bytes.isNotEmpty(subscriberId) && currencyCode ? { subscriberId, currencyCode, - manuallyCancelled: storage.get( + manuallyCancelled: itemStorage.get( 'donorSubscriptionManuallyCancelled', false ), } : null, - svrPin: storage.get('svrPin'), + svrPin: itemStorage.get('svrPin'), accountSettings: { - readReceipts: storage.get('read-receipt-setting'), - sealedSenderIndicators: storage.get('sealedSenderIndicators'), + readReceipts: itemStorage.get('read-receipt-setting'), + sealedSenderIndicators: itemStorage.get('sealedSenderIndicators'), typingIndicators: getTypingIndicatorSetting(), linkPreviews: getLinkPreviewSetting(), notDiscoverableByPhoneNumber: parsePhoneNumberDiscoverability( - storage.get('phoneNumberDiscoverability') + itemStorage.get('phoneNumberDiscoverability') ) === PhoneNumberDiscoverability.NotDiscoverable, - preferContactAvatars: storage.get('preferContactAvatars'), - universalExpireTimerSeconds: storage.get('universalExpireTimer'), + preferContactAvatars: itemStorage.get('preferContactAvatars'), + universalExpireTimerSeconds: itemStorage.get('universalExpireTimer'), preferredReactionEmoji, - displayBadgesOnProfile: storage.get('displayBadgesOnProfile'), - keepMutedChatsArchived: storage.get('keepMutedChatsArchived'), - hasSetMyStoriesPrivacy: storage.get('hasSetMyStoriesPrivacy'), - hasViewedOnboardingStory: storage.get('hasViewedOnboardingStory'), - storiesDisabled: storage.get('hasStoriesDisabled'), - storyViewReceiptsEnabled: storage.get('storyViewReceiptsEnabled'), - hasCompletedUsernameOnboarding: storage.get( + displayBadgesOnProfile: itemStorage.get('displayBadgesOnProfile'), + keepMutedChatsArchived: itemStorage.get('keepMutedChatsArchived'), + hasSetMyStoriesPrivacy: itemStorage.get('hasSetMyStoriesPrivacy'), + hasViewedOnboardingStory: itemStorage.get('hasViewedOnboardingStory'), + storiesDisabled: itemStorage.get('hasStoriesDisabled'), + storyViewReceiptsEnabled: itemStorage.get('storyViewReceiptsEnabled'), + hasCompletedUsernameOnboarding: itemStorage.get( 'hasCompletedUsernameOnboarding' ), - hasSeenGroupStoryEducationSheet: storage.get( + hasSeenGroupStoryEducationSheet: itemStorage.get( 'hasSeenGroupStoryEducationSheet' ), phoneNumberSharingMode, @@ -923,7 +922,11 @@ export class BackupExportStream extends Readable { backupTier: backupTier != null ? Long.fromNumber(backupTier) : null, // Test only values ...(isTestOrMockEnvironment() - ? { optimizeOnDeviceStorage: storage.get('optimizeOnDeviceStorage') } + ? { + optimizeOnDeviceStorage: itemStorage.get( + 'optimizeOnDeviceStorage' + ), + } : {}), }, }; @@ -1077,7 +1080,7 @@ export class BackupExportStream extends Readable { e164, username: convo.username, blocked: convo.serviceId - ? window.storage.blocked.isServiceIdBlocked(convo.serviceId) + ? itemStorage.blocked.isServiceIdBlocked(convo.serviceId) : null, visibility, ...(convo.discoveredUnregisteredAt @@ -1137,7 +1140,7 @@ export class BackupExportStream extends Readable { hideStory: convo.hideStory === true, storySendMode, blocked: convo.groupId - ? window.storage.blocked.isGroupBlocked(convo.groupId) + ? itemStorage.blocked.isGroupBlocked(convo.groupId) : false, avatarColor: toAvatarColor(convo.color), snapshot: { @@ -3032,7 +3035,7 @@ export class BackupExportStream extends Readable { } #toCustomChatColors(): Array { - const customColors = window.storage.get('customColors'); + const customColors = itemStorage.get('customColors'); if (!customColors) { return []; } @@ -3096,16 +3099,16 @@ export class BackupExportStream extends Readable { } #toDefaultChatStyle(): Backups.IChatStyle | null { - const defaultColor = window.storage.get('defaultConversationColor'); - const wallpaperPhotoPointer = window.storage.get( + const defaultColor = itemStorage.get('defaultConversationColor'); + const wallpaperPhotoPointer = itemStorage.get( 'defaultWallpaperPhotoPointer' ); - const wallpaperPreset = window.storage.get('defaultWallpaperPreset'); - const dimWallpaperInDarkMode = window.storage.get( + const wallpaperPreset = itemStorage.get('defaultWallpaperPreset'); + const dimWallpaperInDarkMode = itemStorage.get( 'defaultDimWallpaperInDarkMode', false ); - const autoBubbleColor = window.storage.get('defaultAutoBubbleColor'); + const autoBubbleColor = itemStorage.get('defaultAutoBubbleColor'); return this.#toChatStyle({ wallpaperPhotoPointer, diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts index 20cb17e18b..0c6f056e1f 100644 --- a/ts/services/backups/import.ts +++ b/ts/services/backups/import.ts @@ -157,6 +157,7 @@ import { } from '../../types/NotificationProfile.js'; import { normalizeNotificationProfileId } from '../../types/NotificationProfile-node.js'; import { updateBackupMediaDownloadProgress } from '../../util/updateBackupMediaDownloadProgress.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { isNumber } = lodash; @@ -308,7 +309,7 @@ export class BackupImportStream extends Writable { throw new Error('Missing mediaRootBackupKey'); } - await window.storage.put( + await itemStorage.put( 'restoredBackupFirstAppVersion', info.firstAppVersion ); @@ -318,7 +319,7 @@ export class BackupImportStream extends Writable { if (!constantTimeEqual(theirKey, ourKey)) { // Use root key from integration test if (isTestEnvironment(getEnvironment())) { - await window.storage.put( + await itemStorage.put( 'backupMediaRootKey', info.mediaRootBackupKey ); @@ -383,8 +384,8 @@ export class BackupImportStream extends Writable { await window.ConversationController.load(); await window.ConversationController.checkForConflicts(); - window.storage.reset(); - await window.storage.fetch(); + itemStorage.reset(); + await itemStorage.fetch(); // Load identity keys we just saved. await signalProtocolStore.hydrateCaches(); @@ -426,7 +427,7 @@ export class BackupImportStream extends Writable { { concurrency: MAX_CONCURRENCY } ); - await window.storage.put( + await itemStorage.put( 'pinnedConversationIds', this.#pinnedConversations .sort(([a], [b]) => { @@ -724,10 +725,8 @@ export class BackupImportStream extends Writable { }; this.#ourConversation = me; - const { storage } = window; - strictAssert(Bytes.isNotEmpty(profileKey), 'Missing profile key'); - await storage.put('profileKey', profileKey); + await itemStorage.put('profileKey', profileKey); this.#ourConversation.profileKey = Bytes.toBase64(profileKey); await this.#updateConversation(this.#ourConversation); @@ -738,14 +737,14 @@ export class BackupImportStream extends Writable { if (usernameLink != null) { const { entropy, serverId, color } = usernameLink; if (Bytes.isNotEmpty(entropy) && Bytes.isNotEmpty(serverId)) { - await storage.put('usernameLink', { + await itemStorage.put('usernameLink', { entropy, serverId, }); } // Same numeric value, no conversion needed - await storage.put('usernameLinkColor', color ?? 0); + await itemStorage.put('usernameLinkColor', color ?? 0); } if (givenName != null) { @@ -755,19 +754,19 @@ export class BackupImportStream extends Writable { me.profileFamilyName = familyName; } if (avatarUrlPath != null) { - await storage.put('avatarUrl', avatarUrlPath); + await itemStorage.put('avatarUrl', avatarUrlPath); } if (donationSubscriberData != null) { const { subscriberId, currencyCode, manuallyCancelled } = donationSubscriberData; if (Bytes.isNotEmpty(subscriberId)) { - await storage.put('subscriberId', subscriberId); + await itemStorage.put('subscriberId', subscriberId); } if (currencyCode != null) { - await storage.put('subscriberCurrencyCode', currencyCode); + await itemStorage.put('subscriberCurrencyCode', currencyCode); } if (manuallyCancelled != null) { - await storage.put( + await itemStorage.put( 'donorSubscriptionManuallyCancelled', manuallyCancelled ); @@ -776,88 +775,94 @@ export class BackupImportStream extends Writable { await saveBackupsSubscriberData(backupsSubscriberData); - await storage.put( + await itemStorage.put( 'read-receipt-setting', accountSettings?.readReceipts === true ); - await storage.put( + await itemStorage.put( 'sealedSenderIndicators', accountSettings?.sealedSenderIndicators === true ); - await storage.put( + await itemStorage.put( 'typingIndicators', accountSettings?.typingIndicators === true ); - await storage.put('linkPreviews', accountSettings?.linkPreviews === true); - await storage.put( + await itemStorage.put( + 'linkPreviews', + accountSettings?.linkPreviews === true + ); + await itemStorage.put( 'preferContactAvatars', accountSettings?.preferContactAvatars === true ); if (accountSettings?.universalExpireTimerSeconds) { - await storage.put( + await itemStorage.put( 'universalExpireTimer', accountSettings.universalExpireTimerSeconds ); } - await storage.put( + await itemStorage.put( 'displayBadgesOnProfile', accountSettings?.displayBadgesOnProfile === true ); - await storage.put( + await itemStorage.put( 'keepMutedChatsArchived', accountSettings?.keepMutedChatsArchived === true ); - await storage.put( + await itemStorage.put( 'hasSetMyStoriesPrivacy', accountSettings?.hasSetMyStoriesPrivacy === true ); - await storage.put( + await itemStorage.put( 'hasViewedOnboardingStory', accountSettings?.hasViewedOnboardingStory === true ); - await storage.put( + await itemStorage.put( 'hasStoriesDisabled', accountSettings?.storiesDisabled === true ); // an undefined value for storyViewReceiptsEnabled is semantically different from // false: it causes us to fallback to `read-receipt-setting` - await storage.put( + await itemStorage.put( 'storyViewReceiptsEnabled', accountSettings?.storyViewReceiptsEnabled ?? undefined ); - await storage.put( + await itemStorage.put( 'hasCompletedUsernameOnboarding', accountSettings?.hasCompletedUsernameOnboarding === true ); - await storage.put( + await itemStorage.put( 'hasSeenGroupStoryEducationSheet', accountSettings?.hasSeenGroupStoryEducationSheet === true ); - await storage.put( + await itemStorage.put( 'preferredReactionEmoji', accountSettings?.preferredReactionEmoji || [] ); if (svrPin) { - await storage.put('svrPin', svrPin); + await itemStorage.put('svrPin', svrPin); } if (isTestOrMockEnvironment()) { // Only relevant for tests - await storage.put( + await itemStorage.put( 'optimizeOnDeviceStorage', accountSettings?.optimizeOnDeviceStorage === true ); } this.#backupTier = accountSettings?.backupTier?.toNumber(); - await storage.put('backupTier', accountSettings?.backupTier?.toNumber()); + await itemStorage.put( + 'backupTier', + accountSettings?.backupTier?.toNumber() + ); const { PhoneNumberSharingMode: BackupMode } = Backups.AccountData; switch (accountSettings?.phoneNumberSharingMode) { case BackupMode.EVERYBODY: - await storage.put( + await itemStorage.put( 'phoneNumberSharingMode', PhoneNumberSharingMode.Everybody ); @@ -865,7 +870,7 @@ export class BackupImportStream extends Writable { case BackupMode.UNKNOWN: case BackupMode.NOBODY: default: - await storage.put( + await itemStorage.put( 'phoneNumberSharingMode', PhoneNumberSharingMode.Nobody ); @@ -873,12 +878,12 @@ export class BackupImportStream extends Writable { } if (accountSettings?.notDiscoverableByPhoneNumber) { - await window.storage.put( + await itemStorage.put( 'phoneNumberDiscoverability', PhoneNumberDiscoverability.NotDiscoverable ); } else { - await window.storage.put( + await itemStorage.put( 'phoneNumberDiscoverability', PhoneNumberDiscoverability.Discoverable ); @@ -893,32 +898,32 @@ export class BackupImportStream extends Writable { ); if (defaultChatStyle.color != null) { - await window.storage.put('defaultConversationColor', { + await itemStorage.put('defaultConversationColor', { color: defaultChatStyle.color, customColorData: defaultChatStyle.customColorData, }); } if (defaultChatStyle.wallpaperPhotoPointer != null) { - await window.storage.put( + await itemStorage.put( 'defaultWallpaperPhotoPointer', defaultChatStyle.wallpaperPhotoPointer ); } if (defaultChatStyle.wallpaperPreset != null) { - await window.storage.put( + await itemStorage.put( 'defaultWallpaperPreset', defaultChatStyle.wallpaperPreset ); } if (defaultChatStyle.dimWallpaperInDarkMode != null) { - await window.storage.put( + await itemStorage.put( 'defaultDimWallpaperInDarkMode', defaultChatStyle.dimWallpaperInDarkMode ); } if (defaultChatStyle.autoBubbleColor != null) { - await window.storage.put( + await itemStorage.put( 'defaultAutoBubbleColor', defaultChatStyle.autoBubbleColor ); @@ -1031,10 +1036,10 @@ export class BackupImportStream extends Writable { if (contact.blocked) { if (serviceId) { - await window.storage.blocked.addBlockedServiceId(serviceId); + await itemStorage.blocked.addBlockedServiceId(serviceId); } if (e164) { - await window.storage.blocked.addBlockedNumber(e164); + await itemStorage.blocked.addBlockedNumber(e164); } } @@ -1202,7 +1207,7 @@ export class BackupImportStream extends Writable { this.#pendingGroupAvatars.set(attrs.id, avatarUrl); } if (group.blocked) { - await window.storage.blocked.addBlockedGroup(groupId); + await itemStorage.blocked.addBlockedGroup(groupId); } return attrs; @@ -3626,7 +3631,7 @@ export class BackupImportStream extends Writable { }); } - await window.storage.put('customColors', customColors); + await itemStorage.put('customColors', customColors); } #fromChatStyle(chatStyle: Backups.IChatStyle | null | undefined): Omit< diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index c984671ac9..50f263251a 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -21,6 +21,8 @@ import { createLogger } from '../../logging/log.js'; import * as Bytes from '../../Bytes.js'; import { strictAssert } from '../../util/assert.js'; import { drop } from '../../util/drop.js'; +import { waitForAllBatchers } from '../../util/batcher.js'; +import { flushAllWaitBatchers } from '../../util/waitBatcher.js'; import { DelimitedStream } from '../../util/DelimitedStream.js'; import { appendPaddingStream } from '../../util/logPadding.js'; import { prependStream } from '../../util/prependStream.js'; @@ -86,6 +88,8 @@ import { import { AttachmentLocalBackupManager } from '../../jobs/AttachmentLocalBackupManager.js'; import { decipherWithAesKey } from '../../util/decipherWithAesKey.js'; import { areRemoteBackupsTurnedOn } from '../../util/isBackupEnabled.js'; +import { unlink as unlinkAccount } from '../../textsecure/WebAPI.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { ensureFile } = fsExtra; @@ -195,7 +199,7 @@ export class BackupsService { public async downloadAndImport( options: DownloadOptionsType ): Promise<{ wasBackupImported: boolean }> { - const backupDownloadPath = window.storage.get('backupDownloadPath'); + const backupDownloadPath = itemStorage.get('backupDownloadPath'); if (!backupDownloadPath) { log.warn('backups.downloadAndImport: no backup download path, skipping'); return { wasBackupImported: false }; @@ -203,7 +207,7 @@ export class BackupsService { log.info('backups.downloadAndImport: downloading...'); - const ephemeralKey = window.storage.get('backupEphemeralKey'); + const ephemeralKey = itemStorage.get('backupEphemeralKey'); const absoluteDownloadPath = window.Signal.Migrations.getAbsoluteDownloadsPath(backupDownloadPath); @@ -281,10 +285,10 @@ export class BackupsService { break; } - await window.storage.remove('backupDownloadPath'); - await window.storage.remove('backupEphemeralKey'); - await window.storage.remove('backupTransitArchive'); - await window.storage.put('isRestoredFromBackup', hasBackup); + await itemStorage.remove('backupDownloadPath'); + await itemStorage.remove('backupEphemeralKey'); + await itemStorage.remove('backupTransitArchive'); + await itemStorage.put('isRestoredFromBackup', hasBackup); log.info('backups.downloadAndImport: done'); @@ -789,7 +793,7 @@ export class BackupsService { abortSignal: controller.signal, }); } else { - let archive = window.storage.get('backupTransitArchive'); + let archive = itemStorage.get('backupTransitArchive'); if (archive == null) { const response = await this.api.getTransferArchive(controller.signal); if ('error' in response) { @@ -812,7 +816,7 @@ export class BackupsService { cdn: response.cdn, key: response.key, }; - await window.storage.put('backupTransitArchive', archive); + await itemStorage.put('backupTransitArchive', archive); } stream = await this.api.downloadEphemeral({ @@ -865,10 +869,10 @@ export class BackupsService { try { // Import and start writing to the DB. Make sure we are unlinked // if the import process is aborted due to error or restart. - const password = window.storage.get('password'); + const password = itemStorage.get('password'); strictAssert(password != null, 'Must be registered to import backup'); - await window.storage.remove('password'); + await itemStorage.remove('password'); await this.importFromDisk(downloadPath, { ephemeralKey, @@ -882,7 +886,7 @@ export class BackupsService { }); // Restore password on success - await window.storage.put('password', password); + await itemStorage.put('password', password); } catch (e) { // Error or manual cancel during import; this is non-retriable if (e instanceof BackupInstallerError) { @@ -1005,7 +1009,7 @@ export class BackupsService { }); try { - await window.textsecure.server?.unlink(); + await unlinkAccount(); } catch (e) { log.warn( 'Error while unlinking; this may be expected for the unlink operation', @@ -1046,10 +1050,7 @@ export class BackupsService { await window.waitForEmptyEventQueue(); // Make sure all batches are flushed - await Promise.all([ - window.waitForAllBatchers(), - window.flushAllWaitBatchers(), - ]); + await Promise.all([waitForAllBatchers(), flushAllWaitBatchers()]); } public isImportRunning(): boolean { @@ -1060,7 +1061,7 @@ export class BackupsService { } #getBackupTierFromStorage(): BackupLevel | null { - const backupTier = window.storage.get('backupTier'); + const backupTier = itemStorage.get('backupTier'); switch (backupTier) { case BackupLevel.Free: return BackupLevel.Free; @@ -1086,7 +1087,7 @@ export class BackupsService { }; } - await window.storage.put('cloudBackupStatus', result); + await itemStorage.put('cloudBackupStatus', result); return result; } @@ -1115,7 +1116,7 @@ export class BackupsService { throw missingCaseError(backupTier); } - await window.storage.put('backupSubscriptionStatus', result); + await itemStorage.put('backupSubscriptionStatus', result); return result; } @@ -1129,17 +1130,17 @@ export class BackupsService { async resetCachedData(): Promise { this.api.clearCache(); await this.credentials.clearCache(); - await window.storage.remove('backupSubscriptionStatus'); - await window.storage.remove('cloudBackupStatus'); + await itemStorage.remove('backupSubscriptionStatus'); + await itemStorage.remove('cloudBackupStatus'); await this.refreshBackupAndSubscriptionStatus(); } hasMediaBackups(): boolean { - return window.storage.get('backupTier') === BackupLevel.Paid; + return itemStorage.get('backupTier') === BackupLevel.Paid; } getCachedCloudBackupStatus(): BackupStatusType | undefined { - return window.storage.get('cloudBackupStatus'); + return itemStorage.get('cloudBackupStatus'); } async pickLocalBackupFolder(): Promise { @@ -1150,7 +1151,7 @@ export class BackupsService { return; } - drop(window.storage.put('localBackupFolder', snapshotDir)); + drop(itemStorage.put('localBackupFolder', snapshotDir)); return snapshotDir; } } diff --git a/ts/services/backups/validator.ts b/ts/services/backups/validator.ts index 34684820a5..663fd54692 100644 --- a/ts/services/backups/validator.ts +++ b/ts/services/backups/validator.ts @@ -10,6 +10,7 @@ import protobufjs from 'protobufjs'; import { strictAssert } from '../../util/assert.js'; import { toAciObject } from '../../util/ServiceId.js'; import { missingCaseError } from '../../util/missingCaseError.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { Reader } = protobufjs; @@ -23,10 +24,10 @@ export async function validateBackup( fileSize: number, type: ValidationType ): Promise { - const accountEntropy = window.storage.get('accountEntropyPool'); + const accountEntropy = itemStorage.get('accountEntropyPool'); strictAssert(accountEntropy, 'Account Entropy Pool not available'); - const aci = toAciObject(window.storage.user.getCheckedAci()); + const aci = toAciObject(itemStorage.user.getCheckedAci()); const backupKey = new libsignal.MessageBackupKey({ accountEntropy, aci, diff --git a/ts/services/buildExpiration.ts b/ts/services/buildExpiration.ts index 5a4a632e57..e9ad296d52 100644 --- a/ts/services/buildExpiration.ts +++ b/ts/services/buildExpiration.ts @@ -9,6 +9,7 @@ import { } from '../util/buildExpiration.js'; import { LongTimeout } from '../util/timeout.js'; import { createLogger } from '../logging/log.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('buildExpiration'); @@ -21,7 +22,7 @@ export class BuildExpirationService extends EventEmitter { } hasBuildExpired(): boolean { - const autoDownloadUpdate = window.storage.get('auto-download-update', true); + const autoDownloadUpdate = itemStorage.get('auto-download-update', true); return hasBuildExpired({ buildExpirationTimestamp: this.#getBuildExpirationTimestamp(), @@ -34,12 +35,12 @@ export class BuildExpirationService extends EventEmitter { // Private #getBuildExpirationTimestamp(): number { - const autoDownloadUpdate = window.storage.get('auto-download-update', true); + const autoDownloadUpdate = itemStorage.get('auto-download-update', true); return getBuildExpirationTimestamp({ version: window.getVersion(), packagedBuildExpiration: window.getBuildExpiration(), - remoteBuildExpiration: window.storage.get('remoteBuildExpiration'), + remoteBuildExpiration: itemStorage.get('remoteBuildExpiration'), autoDownloadUpdate, logger: log, }); diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 7d6188cc77..a3145bec99 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -95,6 +95,11 @@ import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary.js'; import { fetchMembershipProof, getMembershipList } from '../groups.js'; import type { ProcessedEnvelope } from '../textsecure/Types.d.ts'; import type { GetIceServersResultType } from '../textsecure/WebAPI.js'; +import { + callLinkCreateAuth, + getIceServers, + makeSfuRequest, +} from '../textsecure/WebAPI.js'; import { missingCaseError } from '../util/missingCaseError.js'; import { normalizeGroupCallTimestamp } from '../util/ringrtc/normalizeGroupCallTimestamp.js'; import { requestCameraPermissions } from '../util/callingPermissions.js'; @@ -148,11 +153,11 @@ import { isServiceIdString, isPniString } from '../types/ServiceId.js'; import { isSignalConnection } from '../util/getSignalConnections.js'; import { toAdminKeyBytes } from '../util/callLinks.js'; import { - getCallLinkAuthCredentialPresentation, getRoomIdFromRootKey, callLinkRestrictionsToRingRTC, callLinkStateFromRingRTC, } from '../util/callLinksRingrtc.js'; +import { getCallLinkAuthCredentialPresentation } from '../util/callLinks/zkgroup.js'; import { conversationJobQueue, conversationQueueJobEnum, @@ -166,6 +171,7 @@ import { getColorForCallLink } from '../util/getColorForCallLink.js'; import OS from '../util/os/osMain.js'; import { sleep } from '../util/sleep.js'; import { signalProtocolStore } from '../SignalProtocolStore.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { uniqBy, noop, compact } = lodash; @@ -246,35 +252,35 @@ export type SetPresentingOptionsType = Readonly<{ }>; function getIncomingCallNotification(): boolean { - return window.storage.get('incoming-call-notification', true); + return itemStorage.get('incoming-call-notification', true); } function getAlwaysRelayCalls(): boolean { - return window.storage.get('always-relay-calls', false); + return itemStorage.get('always-relay-calls', false); } function getPreferredAudioInputDevice(): AudioDevice | undefined { - return window.storage.get('preferred-audio-input-device'); + return itemStorage.get('preferred-audio-input-device'); } async function setPreferredAudioInputDevice( device: AudioDevice ): Promise { - await window.storage.put('preferred-audio-input-device', device); + await itemStorage.put('preferred-audio-input-device', device); } function getPreferredAudioOutputDevice(): AudioDevice | undefined { - return window.storage.get('preferred-audio-output-device'); + return itemStorage.get('preferred-audio-output-device'); } async function setPreferredAudioOutputDevice( device: AudioDevice ): Promise { - await window.storage.put('preferred-audio-output-device', device); + await itemStorage.put('preferred-audio-output-device', device); } function getPreferredVideoInputDevice(): string | undefined { - return window.storage.get('preferred-video-input-device'); + return itemStorage.get('preferred-video-input-device'); } async function setPreferredVideoInputDevice(device: string): Promise { - await window.storage.put('preferred-video-input-device', device); + await itemStorage.put('preferred-video-input-device', device); } function truncateForLogging(name: string | undefined): string | undefined { @@ -563,7 +569,7 @@ export class CallingClass { } #attemptToGiveOurServiceIdToRingRtc(): void { - const ourAci = window.textsecure.storage.user.getAci(); + const ourAci = itemStorage.user.getAci(); if (!ourAci) { // This can happen if we're not linked. It's okay if we hit this case. return; @@ -749,7 +755,7 @@ export class CallingClass { const sfuUrl = this._sfuUrl; const userId = Aci.parseFromServiceIdString( - window.textsecure.storage.user.getCheckedAci() + itemStorage.user.getCheckedAci() ); const rootKey = CallLinkRootKey.generate(); @@ -764,14 +770,8 @@ export class CallingClass { const context = CreateCallLinkCredentialRequestContext.forRoomId(roomId); const requestBase64 = Bytes.toBase64(context.getRequest().serialize()); - strictAssert( - window.textsecure.messaging, - 'createCallLink(): We are offline' - ); const { credential: credentialBase64 } = - await window.textsecure.messaging.server.callLinkCreateAuth( - requestBase64 - ); + await callLinkCreateAuth(requestBase64); const response = new CreateCallLinkCredentialResponse( Bytes.fromBase64(credentialBase64) @@ -1763,7 +1763,7 @@ export class CallingClass { return; } - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const reason = `sendProfileKeysForAdhocCall(${roomId})`; peekInfo.devices.forEach(async device => { const aci = device.userId ? this.#formatUserId(device.userId) : null; @@ -2883,8 +2883,6 @@ export class CallingClass { return; } - const { storage } = window.textsecure; - const senderIdentityRecord = await signalProtocolStore.getOrMigrateIdentityRecord(remoteUserId); if (!senderIdentityRecord) { @@ -2895,7 +2893,7 @@ export class CallingClass { } const senderIdentityKey = senderIdentityRecord.publicKey.subarray(1); // Ignore the type header, it is not used. - const ourAci = storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const receiverIdentityRecord = signalProtocolStore.getIdentityRecord(ourAci); @@ -3167,7 +3165,7 @@ export class CallingClass { return; } - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); if (conversation.get('left') || !conversation.hasMember(ourAci)) { log.warn(`${logId}: we left the group`); @@ -3566,11 +3564,6 @@ export class CallingClass { headers: { [name: string]: string }, body: Uint8Array | undefined ) { - if (!window.textsecure.messaging) { - RingRTC.httpRequestFailed(requestId, 'We are offline'); - return; - } - const httpMethod = RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD.get(method); if (httpMethod === undefined) { RingRTC.httpRequestFailed( @@ -3582,12 +3575,7 @@ export class CallingClass { let result; try { - result = await window.textsecure.messaging.server.makeSfuRequest( - url, - httpMethod, - headers, - body - ); + result = await makeSfuRequest(url, httpMethod, headers, body); } catch (err) { if (err.code !== -1) { // WebAPI treats certain response codes as errors, but RingRTC still needs to @@ -3619,7 +3607,7 @@ export class CallingClass { } get #localDeviceId(): DeviceId | null { - return this.#parseDeviceId(window.textsecure.storage.user.getDeviceId()); + return this.#parseDeviceId(itemStorage.user.getDeviceId()); } #parseDeviceId(deviceId: number | string | undefined): DeviceId | null { @@ -3669,12 +3657,7 @@ export class CallingClass { // Set the default cache expiration time to now + 0. let expirationTimestamp = currentTime; - // The messaging context should have already been checked before entering - // this function, so this should be a noop. - const { messaging } = window.textsecure; - strictAssert(messaging, 'textsecure messaging not available'); - - const iceServerConfig = await messaging.server.getIceServers(); + const iceServerConfig = await getIceServers(); // Advance the next expiration time to the minimum provided ttl value, // or if there were none, use 0 to disable the cache. @@ -3711,10 +3694,6 @@ export class CallingClass { } async #handleStartCall(call: Call): Promise { - if (!window.textsecure.messaging) { - log.error('CallingClass.handleStartCall: offline!'); - return false; - } const conversation = window.ConversationController.get(call.remoteUserId); if (!conversation) { log.error( @@ -3982,7 +3961,7 @@ export class CallingClass { ): Promise { const shouldNotify = !window.SignalContext.activeWindowService.isActive() && - window.storage.get('call-system-notification', true); + itemStorage.get('call-system-notification', true); if (!shouldNotify) { return; diff --git a/ts/services/contactSync.ts b/ts/services/contactSync.ts index 9ebd414de3..06f3aa03f4 100644 --- a/ts/services/contactSync.ts +++ b/ts/services/contactSync.ts @@ -10,6 +10,11 @@ import { parseContactsV2, type ContactDetailsWithAvatar, } from '../textsecure/ContactsParser.js'; +import { + isOnline, + getAttachment, + getAttachmentFromBackupTier, +} from '../textsecure/WebAPI.js'; import * as Conversation from '../types/Conversation.js'; import * as Errors from '../types/errors.js'; import type { ValidateConversationType } from '../model-types.d.ts'; @@ -20,12 +25,12 @@ import { createLogger } from '../logging/log.js'; import { dropNull } from '../util/dropNull.js'; import type { ProcessedAttachment } from '../textsecure/Types.js'; import { downloadAttachment } from '../textsecure/downloadAttachment.js'; -import { strictAssert } from '../util/assert.js'; import type { ReencryptedAttachmentV2 } from '../AttachmentCrypto.js'; import { SECOND } from '../util/durations/index.js'; import { AttachmentVariant } from '../types/Attachment.js'; import { MediaTier } from '../types/AttachmentDownload.js'; import { waitForOnline } from '../util/waitForOnline.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { noop } = lodash; @@ -107,12 +112,14 @@ const queue = new PQueue({ concurrency: 1 }); async function downloadAndParseContactAttachment( contactAttachment: ProcessedAttachment ) { - strictAssert(window.textsecure.server, 'server must exist'); let downloaded: ReencryptedAttachmentV2 | undefined; try { const abortController = new AbortController(); downloaded = await downloadAttachment( - window.textsecure.server, + { + getAttachment, + getAttachmentFromBackupTier, + }, { attachment: contactAttachment, mediaTier: MediaTier.STANDARD }, { variant: AttachmentVariant.Default, @@ -152,10 +159,10 @@ async function doContactSync({ while (contacts === undefined) { attempts += 1; try { - if (!window.textsecure.server?.isOnline()) { + if (!isOnline()) { log.info(`${logId}: We are not online; waiting until we are online`); // eslint-disable-next-line no-await-in-loop - await waitForOnline(); + await waitForOnline({ server: { isOnline } }); log.info(`${logId}: We are back online; starting up again`); } @@ -263,7 +270,7 @@ async function doContactSync({ await Promise.all(promises); - await window.storage.put('synced_at', Date.now()); + await itemStorage.put('synced_at', Date.now()); window.Whisper.events.emit('contactSync:complete'); if (isInitialSync) { isInitialSync = false; diff --git a/ts/services/donations.ts b/ts/services/donations.ts index 49170ee2ea..dd9e5da762 100644 --- a/ts/services/donations.ts +++ b/ts/services/donations.ts @@ -45,6 +45,15 @@ import type { import { ToastType } from '../types/Toast.js'; import { NavTab, SettingsPage } from '../types/Nav.js'; import { getRegionCodeForNumber } from '../util/libphonenumberUtil.js'; +import { + createBoostPaymentIntent, + createPaymentMethodWithStripe, + confirmIntentWithStripe, + createBoostReceiptCredentials, + redeemReceipt, + isOnline, +} from '../textsecure/WebAPI.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { createDonationReceipt } = DataWriter; @@ -322,9 +331,9 @@ export async function _runDonationWorkflow(): Promise { return; } - if (!window.textsecure.server?.isOnline()) { + if (!isOnline()) { log.info(`${logId}: We are not online; waiting until we are online`); - await waitForOnline(); + await waitForOnline({ server: { isOnline } }); log.info(`${logId}: We are back online; starting up again`); } @@ -499,9 +508,6 @@ export async function _createPaymentIntent({ `${logId}: existing workflow at type ${workflow.type} is not at type DONE, unable to create payment intent` ); } - if (!window.textsecure.server) { - throw new Error(`${logId}: window.textsecure.server is not available!`); - } log.info(`${logId}: Creating new workflow`); @@ -511,8 +517,7 @@ export async function _createPaymentIntent({ level: 1, paymentMethod: 'CARD', }; - const { clientSecret } = - await window.textsecure.server.createBoostPaymentIntent(payload); + const { clientSecret } = await createBoostPaymentIntent(payload); const paymentIntentId = clientSecret.split('_secret_')[0]; log.info(`${logId}: Successfully transitioned to INTENT`); @@ -546,16 +551,12 @@ export async function _createPaymentMethodForIntent( `${logId}: workflow at type ${workflow?.type} is not at type INTENT or INTENT_METHOD, unable to create payment method` ); } - if (!window.textsecure.server) { - throw new Error(`${logId}: window.textsecure.server is not available!`); - } log.info(`${logId}: Starting`); - const { id: paymentMethodId } = - await window.textsecure.server.createPaymentMethodWithStripe({ - cardDetail, - }); + const { id: paymentMethodId } = await createPaymentMethodWithStripe({ + cardDetail, + }); log.info(`${logId}: Successfully transitioned to INTENT_METHOD`); @@ -579,9 +580,6 @@ export async function _confirmPayment( `${logId}: workflow at type ${workflow?.type} is not at type INTENT_METHOD, unable to confirm payment` ); } - if (!window.textsecure.server) { - throw new Error(`${logId}: window.textsecure.server is not available!`); - } log.info(`${logId}: Starting`); @@ -603,8 +601,7 @@ export async function _confirmPayment( returnUrl, }; - const { next_action: nextAction } = - await window.textsecure.server.confirmIntentWithStripe(options); + const { next_action: nextAction } = await confirmIntentWithStripe(options); if (nextAction && nextAction.type === 'redirect_to_url') { const { redirect_to_url: redirectDetails } = nextAction; @@ -655,9 +652,6 @@ export async function _completeValidationRedirect( `${logId}: workflow at type ${workflow?.type} is not type INTENT_REDIRECT, unable to complete redirect` ); } - if (!window.textsecure.server) { - throw new Error(`${logId}: window.textsecure.server is not available!`); - } log.info(`${logId}: Starting`); @@ -686,9 +680,6 @@ export async function _getReceipt( `${logId}: workflow at type ${workflow?.type} not type INTENT_CONFIRMED, unable to get receipt` ); } - if (!window.textsecure.server) { - throw new Error(`${logId}: window.textsecure.server is not available!`); - } log.info(`${logId}: Starting`); @@ -709,10 +700,7 @@ export async function _getReceipt( // credentialRequest for the same paymentIntentId let responseWithDetails; try { - responseWithDetails = - await window.textsecure.server.createBoostReceiptCredentials( - jsonPayload - ); + responseWithDetails = await createBoostReceiptCredentials(jsonPayload); } catch (error) { if (error.code === 409) { // Save for the user's tax records even if something went wrong with credential @@ -774,9 +762,6 @@ export async function _redeemReceipt( `${logId}: workflow at type ${workflow?.type} not type RECEIPT, unable to redeem receipt` ); } - if (!window.textsecure.server) { - throw new Error(`${logId}: window.textsecure.server is not available!`); - } log.info(`${logId}: Starting`); @@ -799,7 +784,7 @@ export async function _redeemReceipt( primary: false, }; - await window.textsecure.server.redeemReceipt(jsonPayload); + await redeemReceipt(jsonPayload); // After the receipt credential, our profile will change to add new badges. // Refresh our profile to get new badges. @@ -885,10 +870,10 @@ export function _saveWorkflowToRedux( export function _getWorkflowFromStorage(): DonationWorkflow | undefined { const logId = '_getWorkflowFromStorage'; - const workflowJson = window.storage.get(WORKFLOW_STORAGE_KEY); + const workflowJson = itemStorage.get(WORKFLOW_STORAGE_KEY); if (!workflowJson) { - log.info(`${logId}: No workflow found in window.storage`); + log.info(`${logId}: No workflow found in storage`); return undefined; } @@ -896,7 +881,7 @@ export function _getWorkflowFromStorage(): DonationWorkflow | undefined { const result = safeParseUnknown(donationWorkflowSchema, workflowData); if (!result.success) { log.error( - `${logId}: Workflow from window.storage was malformed: ${result.error.flatten()}` + `${logId}: Workflow from storage was malformed: ${result.error.flatten()}` ); return undefined; } @@ -907,7 +892,7 @@ export function _getWorkflowFromStorage(): DonationWorkflow | undefined { return undefined; } - log.info(`${logId}: Found existing workflow from window.storage`); + log.info(`${logId}: Found existing workflow from storage`); return workflow; } export async function _saveWorkflowToStorage( @@ -916,7 +901,7 @@ export async function _saveWorkflowToStorage( const logId = `_saveWorkflowToStorage(${workflow?.id ? redactId(workflow.id) : 'NONE'}`; if (!workflow) { log.info(`${logId}: Clearing workflow`); - await window.storage.remove(WORKFLOW_STORAGE_KEY); + await itemStorage.remove(WORKFLOW_STORAGE_KEY); return; } @@ -928,8 +913,8 @@ export async function _saveWorkflowToStorage( throw result.error; } - await window.storage.put(WORKFLOW_STORAGE_KEY, JSON.stringify(workflow)); - log.info(`${logId}: Saved workflow to window.storage`); + await itemStorage.put(WORKFLOW_STORAGE_KEY, JSON.stringify(workflow)); + log.info(`${logId}: Saved workflow to storage`); } async function saveReceipt(workflow: DonationWorkflow, logId: string) { diff --git a/ts/services/groupCredentialFetcher.ts b/ts/services/groupCredentialFetcher.ts index 1a76e7df47..bb3aeed643 100644 --- a/ts/services/groupCredentialFetcher.ts +++ b/ts/services/groupCredentialFetcher.ts @@ -11,6 +11,7 @@ import { import { getClientZkAuthOperations } from '../util/zkgroup.js'; import type { GroupCredentialType } from '../textsecure/WebAPI.js'; +import { getGroupCredentials } from '../textsecure/WebAPI.js'; import { strictAssert } from '../util/assert.js'; import * as durations from '../util/durations/index.js'; import { BackOff } from '../util/BackOff.js'; @@ -20,6 +21,7 @@ import { toTaggedPni } from '../types/ServiceId.js'; import { toPniObject, toAciObject } from '../util/ServiceId.js'; import { createLogger } from '../logging/log.js'; import * as Bytes from '../Bytes.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { first, last, sortBy } = lodash; @@ -40,7 +42,7 @@ export type NextCredentialsType = { let started = false; function getCheckedGroupCredentials(reason: string): CredentialsDataType { - const result = window.storage.get('groupCredentials'); + const result = itemStorage.get('groupCredentials'); strictAssert( result !== undefined, `getCheckedCredentials: no credentials found, ${reason}` @@ -51,7 +53,7 @@ function getCheckedGroupCredentials(reason: string): CredentialsDataType { function getCheckedCallLinkAuthCredentials( reason: string ): CredentialsDataType { - const result = window.storage.get('callLinkAuthCredentials'); + const result = itemStorage.get('callLinkAuthCredentials'); strictAssert( result !== undefined, `getCheckedCallLinkAuthCredentials: no credentials found, ${reason}` @@ -154,7 +156,7 @@ export function getCheckedCallLinkAuthCredentialsForToday( export async function maybeFetchNewCredentials(): Promise { const logId = 'maybeFetchNewCredentials'; - const maybeAci = window.textsecure.storage.user.getAci(); + const maybeAci = itemStorage.user.getAci(); if (!maybeAci) { log.info(`${logId}: no ACI, returning early`); return; @@ -162,19 +164,13 @@ export async function maybeFetchNewCredentials(): Promise { const aci = maybeAci; const prevGroupCredentials: CredentialsDataType = - window.storage.get('groupCredentials') ?? []; + itemStorage.get('groupCredentials') ?? []; const prevCallLinkAuthCredentials: CredentialsDataType = - window.storage.get('callLinkAuthCredentials') ?? []; + itemStorage.get('callLinkAuthCredentials') ?? []; const requestDates = getDatesForRequest(prevGroupCredentials); const requestDatesCallLinks = getDatesForRequest(prevCallLinkAuthCredentials); - const { server } = window.textsecure; - if (!server) { - log.error(`${logId}: unable to get server`); - return; - } - let startDayInMs: number; let endDayInMs: number; if (requestDates) { @@ -206,14 +202,14 @@ export async function maybeFetchNewCredentials(): Promise { pni: untaggedPni, credentials: rawCredentials, callLinkAuthCredentials, - } = await server.getGroupCredentials({ startDayInMs, endDayInMs }); + } = await getGroupCredentials({ startDayInMs, endDayInMs }); strictAssert( untaggedPni, 'Server must give pni along with group credentials' ); const pni = toTaggedPni(untaggedPni); - const localPni = window.storage.user.getPni(); + const localPni = itemStorage.user.getPni(); if (pni !== localPni) { log.error(`${logId}: local PNI ${localPni}, does not match remote ${pni}`); } @@ -303,8 +299,8 @@ export async function maybeFetchNewCredentials(): Promise { )}` ); - await window.storage.put('groupCredentials', finalGroupCredentials); - await window.storage.put( + await itemStorage.put('groupCredentials', finalGroupCredentials); + await itemStorage.put( 'callLinkAuthCredentials', finalCallLinkAuthCredentials ); diff --git a/ts/services/notifications.ts b/ts/services/notifications.ts index 97a7c21306..724eb01b09 100644 --- a/ts/services/notifications.ts +++ b/ts/services/notifications.ts @@ -8,6 +8,7 @@ import { v4 as getGuid } from 'uuid'; import { Sound, SoundType } from '../util/Sound.js'; import { shouldHideExpiringMessageBody } from '../types/Settings.js'; +import { itemStorage as fallbackStorage } from '../textsecure/Storage.js'; import OS from '../util/os/osMain.js'; import { createLogger } from '../logging/log.js'; import { makeEnumParser } from '../util/enum.js'; @@ -121,9 +122,9 @@ class NotificationService extends EventEmitter { } log.error( - 'NotificationService not initialized. Falling back to window.storage, but you should fix this' + 'NotificationService not initialized. Falling back to storage, but you should fix this' ); - return window.storage; + return fallbackStorage; } #getI18n(): LocalizerType { diff --git a/ts/services/profiles.ts b/ts/services/profiles.ts index e3b325112e..47636152f2 100644 --- a/ts/services/profiles.ts +++ b/ts/services/profiles.ts @@ -12,7 +12,8 @@ import type { ReadonlyDeep } from 'type-fest'; import type { ConversationModel } from '../models/conversations.js'; import type { CapabilitiesType } from '../types/Capabilities.d.ts'; import type { ProfileType } from '../textsecure/WebAPI.js'; -import MessageSender from '../textsecure/SendMessage.js'; +import { getProfile, getProfileUnauth } from '../textsecure/WebAPI.js'; +import { MessageSender } from '../textsecure/SendMessage.js'; import type { ServiceIdString } from '../types/ServiceId.js'; import { DataWriter } from '../sql/Client.js'; import { createLogger } from '../logging/log.js'; @@ -46,6 +47,7 @@ import { } from '../util/groupSendEndorsements.js'; import { ProfileDecryptError } from '../types/errors.js'; import { signalProtocolStore } from '../SignalProtocolStore.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('profiles'); @@ -485,11 +487,6 @@ async function doGetProfile( const logId = groupId ? `getProfile(${c.idForLogging()} in groupv2(${groupId}))` : `getProfile(${c.idForLogging()})`; - const { messaging } = window.textsecure; - strictAssert( - messaging, - `${logId}: window.textsecure.messaging not available` - ); const { updatesUrl } = window.SignalContext.config; strictAssert( @@ -530,9 +527,9 @@ async function doGetProfile( let profile: ProfileType; try { if (request.accessKey != null || request.groupSendToken != null) { - profile = await messaging.server.getProfileUnauth(serviceId, request); + profile = await getProfileUnauth(serviceId, request); } else { - profile = await messaging.server.getProfile(serviceId, request); + profile = await getProfile(serviceId, request); } } catch (error) { if (error instanceof HTTPError) { @@ -691,7 +688,7 @@ async function doGetProfile( // Step #: Save our own `paymentAddress` to Storage if (isFieldDefined(profile.paymentAddress) && isMe(c.attributes)) { - await window.storage.put('paymentAddress', profile.paymentAddress); + await itemStorage.put('paymentAddress', profile.paymentAddress); } // Step #: Save profile `capabilities` to conversation @@ -708,7 +705,7 @@ async function doGetProfile( let hasChanged = false; const observedCapabilities = { - ...window.storage.get('observedCapabilities'), + ...itemStorage.get('observedCapabilities'), }; const newKeys = new Array(); for (const key of OBSERVED_CAPABILITY_KEYS) { @@ -726,7 +723,7 @@ async function doGetProfile( } } - await window.storage.put('observedCapabilities', observedCapabilities); + await itemStorage.put('observedCapabilities', observedCapabilities); if (hasChanged) { log.info( 'getProfile: detected a capability flip, sending fetch profile', @@ -865,7 +862,7 @@ export async function updateIdentityKey( log.info(`updateIdentityKey(${serviceId}): changed`); // save identity will close all sessions except for .1, so we // must close that one manually. - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); await signalProtocolStore.archiveSession( new QualifiedAddress(ourAci, new Address(serviceId, 1)) ); diff --git a/ts/services/releaseNotesFetcher.ts b/ts/services/releaseNotesFetcher.ts index c358be1665..8e95d38505 100644 --- a/ts/services/releaseNotesFetcher.ts +++ b/ts/services/releaseNotesFetcher.ts @@ -23,11 +23,18 @@ import { BodyRange } from '../types/BodyRange.js'; import type { ReleaseNotesManifestResponseType, ReleaseNoteResponseType, + isOnline as doIsOnline, + getReleaseNote as doGetReleaseNote, + getReleaseNoteHash as doGetReleaseNoteHash, + getReleaseNoteImageAttachment as doGetReleaseNoteImageAttachment, + getReleaseNotesManifest as doGetReleaseNotesManifest, + getReleaseNotesManifestHash as doGetReleaseNotesManifestHash, } from '../textsecure/WebAPI.js'; import type { WithRequiredProperties } from '../types/Util.js'; import { MessageModel } from '../models/messages.js'; import { stringToMIMEType } from '../types/MIME.js'; import { isNotNil } from '../util/isNotNil.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { last } = lodash; @@ -62,17 +69,29 @@ const STYLE_MAPPING: Record = { spoiler: BodyRange.Style.SPOILER, mono: BodyRange.Style.MONOSPACE, }; + +export type ServerType = Readonly<{ + isOnline: typeof doIsOnline; + getReleaseNote: typeof doGetReleaseNote; + getReleaseNoteHash: typeof doGetReleaseNoteHash; + getReleaseNoteImageAttachment: typeof doGetReleaseNoteImageAttachment; + getReleaseNotesManifest: typeof doGetReleaseNotesManifest; + getReleaseNotesManifestHash: typeof doGetReleaseNotesManifestHash; +}>; + export class ReleaseNotesFetcher { static initComplete = false; #timeout: NodeJS.Timeout | undefined; #isRunning = false; + #server: ServerType; + + constructor(server: ServerType) { + this.#server = server; + } protected setTimeoutForNextRun(options?: FetchOptions): void { const now = Date.now(); - const time = window.textsecure.storage.get( - NEXT_FETCH_TIME_STORAGE_KEY, - now - ); + const time = itemStorage.get(NEXT_FETCH_TIME_STORAGE_KEY, now); log.info('Next update scheduled for', new Date(time).toISOString()); @@ -86,32 +105,20 @@ export class ReleaseNotesFetcher { } #getOrInitializeVersionWatermark(): string { - const versionWatermark = window.textsecure.storage.get( - VERSION_WATERMARK_STORAGE_KEY - ); + const versionWatermark = itemStorage.get(VERSION_WATERMARK_STORAGE_KEY); if (versionWatermark) { return versionWatermark; } log.info('Initializing version high watermark to current version'); const currentVersion = window.getVersion(); - drop( - window.textsecure.storage.put( - VERSION_WATERMARK_STORAGE_KEY, - currentVersion - ) - ); + drop(itemStorage.put(VERSION_WATERMARK_STORAGE_KEY, currentVersion)); return currentVersion; } async #getReleaseNote( note: ManifestReleaseNoteType ): Promise { - if (!window.textsecure.server) { - log.info('WebAPI unavailable'); - throw new Error('WebAPI unavailable'); - } - const { uuid, ctaId, link } = note; const globalLocale = new Intl.Locale(window.SignalContext.getI18nLocale()); const localesToTry = [ @@ -123,7 +130,7 @@ export class ReleaseNotesFetcher { for (const localeToTry of localesToTry) { try { // eslint-disable-next-line no-await-in-loop - const hash = await window.textsecure.server.getReleaseNoteHash({ + const hash = await this.#server.getReleaseNoteHash({ uuid, locale: localeToTry, }); @@ -133,7 +140,7 @@ export class ReleaseNotesFetcher { } // eslint-disable-next-line no-await-in-loop - const result = await window.textsecure.server.getReleaseNote({ + const result = await this.#server.getReleaseNote({ uuid, locale: localeToTry, }); @@ -163,11 +170,6 @@ export class ReleaseNotesFetcher { async #processReleaseNotes( notes: ReadonlyArray ): Promise { - if (!window.textsecure.server) { - log.info('WebAPI unavailable'); - throw new Error('WebAPI unavailable'); - } - log.info('Ensuring Signal conversation'); const signalConversation = await window.ConversationController.getOrCreateSignalConversation(); @@ -186,22 +188,13 @@ export class ReleaseNotesFetcher { log.info( `Signal conversation is blocked, updating watermark to ${versionWatermark}` ); - drop( - window.textsecure.storage.put( - VERSION_WATERMARK_STORAGE_KEY, - versionWatermark - ) - ); + drop(itemStorage.put(VERSION_WATERMARK_STORAGE_KEY, versionWatermark)); return; } const hydratedNotesWithRawAttachments = ( await Promise.all( sortedNotes.map(async note => { - if (!window.textsecure.server) { - log.info('WebAPI unavailable'); - throw new Error('WebAPI unavailable'); - } if (!note) { return null; } @@ -212,7 +205,7 @@ export class ReleaseNotesFetcher { } if (hydratedNote.media) { const { imageData: rawAttachmentData, contentType } = - await window.textsecure.server.getReleaseNoteImageAttachment( + await this.#server.getReleaseNoteImageAttachment( hydratedNote.media ); @@ -337,12 +330,7 @@ export class ReleaseNotesFetcher { signalConversation.throttledUpdateUnread(); log.info(`Updating version watermark to ${versionWatermark}`); - drop( - window.textsecure.storage.put( - VERSION_WATERMARK_STORAGE_KEY, - versionWatermark - ) - ); + drop(itemStorage.put(VERSION_WATERMARK_STORAGE_KEY, versionWatermark)); } async #scheduleForNextRun(options?: { @@ -350,7 +338,7 @@ export class ReleaseNotesFetcher { }): Promise { const now = Date.now(); const nextTime = options?.isNewVersion ? now : now + FETCH_INTERVAL; - await window.textsecure.storage.put(NEXT_FETCH_TIME_STORAGE_KEY, nextTime); + await itemStorage.put(NEXT_FETCH_TIME_STORAGE_KEY, nextTime); } async #run(options?: FetchOptions): Promise { @@ -365,19 +353,12 @@ export class ReleaseNotesFetcher { const versionWatermark = this.#getOrInitializeVersionWatermark(); log.info(`Version watermark is ${versionWatermark}`); - if (!window.textsecure.server) { - log.info('WebAPI unavailable'); - throw new Error('WebAPI unavailable'); - } - - const hash = await window.textsecure.server.getReleaseNotesManifestHash(); + const hash = await this.#server.getReleaseNotesManifestHash(); if (!hash) { throw new Error('Release notes manifest hash missing'); } - const previousHash = window.textsecure.storage.get( - PREVIOUS_MANIFEST_HASH_STORAGE_KEY - ); + const previousHash = itemStorage.get(PREVIOUS_MANIFEST_HASH_STORAGE_KEY); if (hash !== previousHash || options?.isNewVersion) { log.info( @@ -385,8 +366,7 @@ export class ReleaseNotesFetcher { options?.isNewVersion ? 'true' : 'false' }, hashChanged=${hash !== previousHash ? 'true' : 'false'}` ); - const manifest = - await window.textsecure.server.getReleaseNotesManifest(); + const manifest = await this.#server.getReleaseNotesManifest(); const currentVersion = window.getVersion(); const validNotes = manifest.announcements.filter( (note): note is ManifestReleaseNoteType => @@ -401,12 +381,7 @@ export class ReleaseNotesFetcher { log.info('No new release notes'); } - drop( - window.textsecure.storage.put( - PREVIOUS_MANIFEST_HASH_STORAGE_KEY, - hash - ) - ); + drop(itemStorage.put(PREVIOUS_MANIFEST_HASH_STORAGE_KEY, hash)); } else { log.info('Manifest hash unchanged, aborting fetch'); } @@ -427,7 +402,7 @@ export class ReleaseNotesFetcher { } #runWhenOnline(options?: FetchOptions) { - if (window.textsecure.server?.isOnline()) { + if (this.#server.isOnline()) { drop(this.#run(options)); } else { log.info('We are offline; will fetch when we are next online'); @@ -440,6 +415,7 @@ export class ReleaseNotesFetcher { } public static async init( + server: ServerType, events: MinimalEventsType, isNewVersion: boolean ): Promise { @@ -449,7 +425,7 @@ export class ReleaseNotesFetcher { ReleaseNotesFetcher.initComplete = true; - const listener = new ReleaseNotesFetcher(); + const listener = new ReleaseNotesFetcher(server); if (isNewVersion) { await listener.#scheduleForNextRun({ isNewVersion }); diff --git a/ts/services/retryPlaceholders.ts b/ts/services/retryPlaceholders.ts index ddc3fb6586..a3360f72a4 100644 --- a/ts/services/retryPlaceholders.ts +++ b/ts/services/retryPlaceholders.ts @@ -49,13 +49,13 @@ export class RetryPlaceholders { #byConversation: ByConversationLookupType = {}; #byMessage: ByMessageLookupType = new Map(); #retryReceiptLifespan: number; - #storage: StorageInterface | undefined; + #storage: Pick | undefined; constructor(options: { retryReceiptLifespan?: number } = {}) { this.#retryReceiptLifespan = options.retryReceiptLifespan || HOUR; } - start(storage: StorageInterface): void { + start(storage: Pick): void { if (this.#isStarted) { throw new Error('RetryPlaceholders: already started'); } diff --git a/ts/services/senderCertificate.ts b/ts/services/senderCertificate.ts index 8bedc16490..25a43d375f 100644 --- a/ts/services/senderCertificate.ts +++ b/ts/services/senderCertificate.ts @@ -15,7 +15,7 @@ import { waitForOnline } from '../util/waitForOnline.js'; import { createLogger } from '../logging/log.js'; import type { StorageInterface } from '../types/Storage.d.ts'; import * as Errors from '../types/errors.js'; -import type { WebAPIType } from '../textsecure/WebAPI.js'; +import type { isOnline, getSenderCertificate } from '../textsecure/WebAPI.js'; import { safeParseUnknown } from '../util/schemas.js'; const log = createLogger('senderCertificate'); @@ -27,9 +27,14 @@ function isWellFormed(data: unknown): data is SerializedCertificateType { // In case your clock is different from the server's, we "fake" expire certificates early. const CLOCK_SKEW_THRESHOLD = 15 * 60 * 1000; +type ServerType = Readonly<{ + isOnline: typeof isOnline; + getSenderCertificate: typeof getSenderCertificate; +}>; + // This is exported for testing. export class SenderCertificateService { - #server?: WebAPIType; + #server?: ServerType; #fetchPromises: Map< SenderCertificateMode, @@ -44,7 +49,7 @@ export class SenderCertificateService { events, storage, }: { - server: WebAPIType; + server: ServerType; events?: Pick; storage: StorageInterface; }): void { diff --git a/ts/services/storage.ts b/ts/services/storage.ts index 8f5fc04b7c..af4431dab0 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -65,7 +65,13 @@ import type { RemoteRecord, UnknownRecord, } from '../types/StorageService.d.ts'; -import MessageSender from '../textsecure/SendMessage.js'; +import { + modifyStorageRecords, + getStorageCredentials, + getStorageManifest, + getStorageRecords, +} from '../textsecure/WebAPI.js'; +import { MessageSender } from '../textsecure/SendMessage.js'; import type { StoryDistributionWithMembersType, StorageServiceFieldsType, @@ -93,6 +99,7 @@ import { validateConversation } from '../util/validateConversation.js'; import { hasAllChatsChatFolder } from '../types/ChatFolder.js'; import type { ChatFolder } from '../types/ChatFolder.js'; import type { NotificationProfileType } from '../types/NotificationProfile.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { debounce, isNumber, chunk } = lodash; @@ -152,7 +159,7 @@ function encryptRecord( ? Bytes.fromBase64(storageID) : generateStorageID(); - const storageKeyBase64 = window.storage.get('storageKey'); + const storageKeyBase64 = itemStorage.get('storageKey'); if (!storageKeyBase64) { throw new Error('No storage key'); } @@ -196,7 +203,7 @@ async function generateManifest( await window.ConversationController.checkForConflicts(); // Load at the beginning, so we use this one value through the whole process - const notificationProfileSyncDisabled = window.storage.get( + const notificationProfileSyncDisabled = itemStorage.get( 'notificationProfileSyncDisabled', false ); @@ -731,7 +738,7 @@ async function generateManifest( }); const unknownRecordsArray: ReadonlyArray = ( - window.storage.get('storage-service-unknown-records') || [] + itemStorage.get('storage-service-unknown-records') || [] ).filter((record: UnknownRecord) => !validRecordTypes.has(record.itemType)); const redactedUnknowns = unknownRecordsArray.map(redactExtendedStorageID); @@ -748,7 +755,7 @@ async function generateManifest( recordsByID.set(record.storageID, record); }); - const recordsWithErrors: ReadonlyArray = window.storage.get( + const recordsWithErrors: ReadonlyArray = itemStorage.get( 'storage-service-error-records', new Array() ); @@ -766,7 +773,7 @@ async function generateManifest( }); // Delete keys that we wanted to drop during the processing of the manifest. - const storedPendingDeletes = window.storage.get( + const storedPendingDeletes = itemStorage.get( 'storage-service-pending-deletes', [] ); @@ -882,7 +889,7 @@ async function generateManifest( }); // Save pending deletes until we have a confirmed upload - await window.storage.put( + await itemStorage.put( 'storage-service-pending-deletes', // Note: `deleteKeys` already includes the prev value of // 'storage-service-pending-deletes' @@ -930,7 +937,7 @@ async function generateManifest( recordIkm = previousManifest.recordIkm; } } else { - recordIkm = window.storage.get('manifestRecordIkm'); + recordIkm = itemStorage.get('manifestRecordIkm'); } return { @@ -991,13 +998,13 @@ async function encryptManifest( const manifestRecord = new Proto.ManifestRecord(); manifestRecord.version = Long.fromNumber(version); - manifestRecord.sourceDevice = window.storage.user.getDeviceId() ?? 0; + manifestRecord.sourceDevice = itemStorage.user.getDeviceId() ?? 0; manifestRecord.identifiers = Array.from(manifestRecordKeys); if (recordIkm != null) { manifestRecord.recordIkm = recordIkm; } - const storageKeyBase64 = window.storage.get('storageKey'); + const storageKeyBase64 = itemStorage.get('storageKey'); if (!storageKeyBase64) { throw new Error('No storage key'); } @@ -1026,16 +1033,12 @@ async function uploadManifest( { postUploadUpdateFunctions, deleteKeys }: GeneratedManifestType, { newItems, storageManifest }: EncryptedManifestType ): Promise { - if (!window.textsecure.messaging) { - throw new Error('storageService.uploadManifest: We are offline!'); - } - if (newItems.size === 0 && deleteKeys.size === 0) { log.info(`upload(${version}): nothing to upload`); return; } - const credentials = window.storage.get('storageCredentials'); + const credentials = itemStorage.get('storageCredentials'); try { log.info( `upload(${version}): inserting=${newItems.size} ` + @@ -1049,7 +1052,7 @@ async function uploadManifest( Bytes.fromBase64(storageID) ); - await window.textsecure.messaging.modifyStorageRecords( + await modifyStorageRecords( Proto.WriteOperation.encode(writeOperation).finish(), { credentials, @@ -1084,7 +1087,7 @@ async function uploadManifest( } log.info(`upload(${version}): setting new manifestVersion`); - await window.storage.put('manifestVersion', version); + await itemStorage.put('manifestVersion', version); conflictBackOff.reset(); backOff.reset(); @@ -1094,7 +1097,7 @@ async function uploadManifest( async function stopStorageServiceSync(reason: Error) { log.warn('stopStorageServiceSync', Errors.toLogFormat(reason)); - await window.storage.remove('storageKey'); + await itemStorage.remove('storageKey'); if (backOff.isFull()) { log.warn('stopStorageServiceSync: too many consecutive stops'); @@ -1117,7 +1120,7 @@ async function stopStorageServiceSync(reason: Error) { async function createNewManifest() { log.info('createNewManifest: creating new manifest'); - const version = window.storage.get('manifestVersion', 0); + const version = itemStorage.get('manifestVersion', 0); const generatedManifest = await generateManifest(version, undefined, true); @@ -1139,7 +1142,7 @@ async function decryptManifest( ): Promise { const { version, value } = encryptedManifest; - const storageKeyBase64 = window.storage.get('storageKey'); + const storageKeyBase64 = itemStorage.get('storageKey'); if (!storageKeyBase64) { throw new Error('No storage key'); } @@ -1161,21 +1164,14 @@ async function fetchManifest( const logId = `sync(${manifestVersion})`; log.info(`${logId}: fetch start`); - if (!window.textsecure.messaging) { - throw new Error('storageService.sync: we are offline!'); - } - try { - const credentials = - await window.textsecure.messaging.getStorageCredentials(); - await window.storage.put('storageCredentials', credentials); + const credentials = await getStorageCredentials(); + await itemStorage.put('storageCredentials', credentials); - const manifestBinary = await window.textsecure.messaging.getStorageManifest( - { - credentials, - greaterThanVersion: manifestVersion, - } - ); + const manifestBinary = await getStorageManifest({ + credentials, + greaterThanVersion: manifestVersion, + }); const encryptedManifest = Proto.StorageManifest.decode(manifestBinary); try { @@ -1398,10 +1394,6 @@ async function processManifest( manifest: Proto.IManifestRecord, version: number ): Promise { - if (!window.textsecure.messaging) { - throw new Error('storageService.processManifest: We are offline!'); - } - const remoteKeysTypeMap = new Map(); (manifest.identifiers || []).forEach( ({ raw, type }: IManifestRecordIdentifier) => { @@ -1472,7 +1464,7 @@ async function processManifest( } const unknownRecordsArray: ReadonlyArray = - window.storage.get('storage-service-unknown-records') || []; + itemStorage.get('storage-service-unknown-records') || []; const stillUnknown = unknownRecordsArray.filter((record: UnknownRecord) => { // Do not include any unknown records that we already support @@ -1784,14 +1776,10 @@ async function fetchRemoteRecords( recordIkm: Uint8Array | undefined, remoteOnlyRecords: Map ): Promise { - const storageKeyBase64 = window.storage.get('storageKey'); + const storageKeyBase64 = itemStorage.get('storageKey'); if (!storageKeyBase64) { throw new Error('No storage key'); } - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error('messaging is not available'); - } const storageKey = Bytes.fromBase64(storageKeyBase64); @@ -1800,7 +1788,7 @@ async function fetchRemoteRecords( `fetching remote keys count=${remoteOnlyRecords.size}` ); - const credentials = window.storage.get('storageCredentials'); + const credentials = itemStorage.get('storageCredentials'); const batches = chunk(Array.from(remoteOnlyRecords.keys()), MAX_READ_KEYS); const storageItems = ( @@ -1812,7 +1800,7 @@ async function fetchRemoteRecords( const readOperation = new Proto.ReadOperation(); readOperation.readKey = batch.map(Bytes.fromBase64); - const storageItemsBuffer = await messaging.getStorageRecords( + const storageItemsBuffer = await getStorageRecords( Proto.ReadOperation.encode(readOperation).finish(), { credentials, @@ -2096,7 +2084,7 @@ async function processRemoteRecords( const unknownRecords: Map = new Map(); const previousUnknownRecords: ReadonlyArray = - window.storage.get( + itemStorage.get( 'storage-service-unknown-records', new Array() ); @@ -2146,10 +2134,7 @@ async function processRemoteRecords( `unknown records=${JSON.stringify(redactedNewUnknowns)} ` + `count=${redactedNewUnknowns.length}` ); - await window.storage.put( - 'storage-service-unknown-records', - newUnknownRecords - ); + await itemStorage.put('storage-service-unknown-records', newUnknownRecords); const redactedErrorRecords = newRecordsWithErrors.map( redactExtendedStorageID @@ -2162,7 +2147,7 @@ async function processRemoteRecords( // Refresh the list of records that had errors with every push, that way // this list doesn't grow unbounded and we keep the list of storage keys // fresh. - await window.storage.put( + await itemStorage.put( 'storage-service-error-records', newRecordsWithErrors ); @@ -2179,7 +2164,7 @@ async function processRemoteRecords( `pending deletes=${JSON.stringify(redactedPendingDeletes)} ` + `count=${redactedPendingDeletes.length}` ); - await window.storage.put('storage-service-pending-deletes', pendingDeletes); + await itemStorage.put('storage-service-pending-deletes', pendingDeletes); } catch (err) { log.error( `process(${storageVersion}): failed to process remote records`, @@ -2193,8 +2178,8 @@ async function sync({ }: { reason: string; }): Promise { - if (!window.storage.get('storageKey')) { - const masterKeyBase64 = window.storage.get('masterKey'); + if (!itemStorage.get('storageKey')) { + const masterKeyBase64 = itemStorage.get('masterKey'); if (!masterKeyBase64) { log.error(`sync(${reason}): Cannot start; no storage or master key!`); return; @@ -2202,7 +2187,7 @@ async function sync({ const masterKey = Bytes.fromBase64(masterKeyBase64); const storageKeyBase64 = Bytes.toBase64(deriveStorageServiceKey(masterKey)); - await window.storage.put('storageKey', storageKeyBase64); + await itemStorage.put('storageKey', storageKeyBase64); log.warn('sync: fixed storage key'); } @@ -2212,10 +2197,10 @@ async function sync({ let manifest: Proto.ManifestRecord | undefined; try { // If we've previously interacted with storage service, update 'fetchComplete' record - const previousFetchComplete = window.storage.get('storageFetchComplete'); - const manifestFromStorage = window.storage.get('manifestVersion'); + const previousFetchComplete = itemStorage.get('storageFetchComplete'); + const manifestFromStorage = itemStorage.get('manifestVersion'); if (!previousFetchComplete && isNumber(manifestFromStorage)) { - await window.storage.put('storageFetchComplete', true); + await itemStorage.put('storageFetchComplete', true); } const localManifestVersion = manifestFromStorage || 0; @@ -2244,15 +2229,15 @@ async function sync({ log.info(`sync: updated to version=${version}`); - await window.storage.put('manifestVersion', version); + await itemStorage.put('manifestVersion', version); if (Bytes.isNotEmpty(manifest.recordIkm)) { - await window.storage.put('manifestRecordIkm', manifest.recordIkm); + await itemStorage.put('manifestRecordIkm', manifest.recordIkm); } else { - await window.storage.remove('manifestRecordIkm'); + await itemStorage.remove('manifestRecordIkm'); } // We now know that we've successfully completed a storage service fetch - await window.storage.put('storageFetchComplete', true); + await itemStorage.put('storageFetchComplete', true); if (window.SignalCI) { window.SignalCI.handleEvent('storageServiceComplete', { @@ -2277,10 +2262,6 @@ async function upload({ }): Promise { const logId = `storageService.upload/${reason}`; - if (!window.textsecure.messaging) { - throw new Error(`${logId}: We are offline!`); - } - // Rate limit uploads coming from syncing if (fromSync) { uploadBucket.push(Date.now()); @@ -2295,7 +2276,7 @@ async function upload({ } } - if (!window.storage.get('storageKey')) { + if (!itemStorage.get('storageKey')) { // requesting new keys runs the sync job which will detect the conflict // and re-run the upload job once we're merged and up-to-date. backOff.reset(); @@ -2326,7 +2307,7 @@ async function upload({ }); } - const localManifestVersion = window.storage.get('manifestVersion', 0); + const localManifestVersion = itemStorage.get('manifestVersion', 0); const version = Number(localManifestVersion) + 1; log.info(`${logId}/${version}: will update to manifest version`); @@ -2341,7 +2322,7 @@ async function upload({ await uploadManifest(version, generatedManifest, encryptedManifest); // Clear pending delete keys after successful upload - await window.storage.put('storage-service-pending-deletes', []); + await itemStorage.put('storage-service-pending-deletes', []); } catch (err) { if (err.code === 409) { await sleep(conflictBackOff.getAndIncrement()); @@ -2392,12 +2373,12 @@ export async function eraseAllStorageServiceState({ // First, update high-level storage service metadata await Promise.all([ - window.storage.remove('manifestVersion'), - window.storage.remove('manifestRecordIkm'), + itemStorage.remove('manifestVersion'), + itemStorage.remove('manifestRecordIkm'), keepUnknownFields ? Promise.resolve() - : window.storage.remove('storage-service-unknown-records'), - window.storage.remove('storageCredentials'), + : itemStorage.remove('storage-service-unknown-records'), + itemStorage.remove('storageCredentials'), ]); // Then, we make the changes to records in memory: @@ -2435,7 +2416,7 @@ export async function eraseAllStorageServiceState({ export async function reprocessUnknownFields(): Promise { ourProfileKeyService.blockGetWithPromise( storageJobQueue(async () => { - const version = window.storage.get('manifestVersion') ?? 0; + const version = itemStorage.get('manifestVersion') ?? 0; log.info(`reprocessUnknownFields(${version}): starting`); @@ -2507,7 +2488,7 @@ export const storageServiceUploadJob = debounce( async () => { await upload({ reason: `storageServiceUploadJob/${reason}` }); }, - `upload v${window.storage.get('manifestVersion')}` + `upload v${itemStorage.get('manifestVersion')}` ); }, isMockEnvironment() ? 0 : 500 @@ -2528,7 +2509,7 @@ export const runStorageServiceSyncJob = debounce( // Notify listeners about sync completion window.Whisper.events.emit('storageService:syncComplete'); }, - `sync v${window.storage.get('manifestVersion')}` + `sync v${itemStorage.get('manifestVersion')}` ) ); }, @@ -2538,11 +2519,11 @@ export const runStorageServiceSyncJob = debounce( export const addPendingDelete = (item: ExtendedStorageID): void => { void storageJobQueue( async () => { - const storedPendingDeletes = window.storage.get( + const storedPendingDeletes = itemStorage.get( 'storage-service-pending-deletes', [] ); - await window.storage.put('storage-service-pending-deletes', [ + await itemStorage.put('storage-service-pending-deletes', [ ...storedPendingDeletes, item, ]); diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 8333b012b1..6c7408a143 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -97,12 +97,12 @@ import { fromPniUuidBytesOrUntaggedString, } from '../util/ServiceId.js'; import { isProtoBinaryEncodingEnabled } from '../util/isProtoBinaryEncodingEnabled.js'; -import { getLinkPreviewSetting } from '../types/LinkPreview.js'; import { + getLinkPreviewSetting, getReadReceiptSetting, getSealedSenderIndicatorSetting, getTypingIndicatorSetting, -} from '../types/Util.js'; +} from '../util/Settings.js'; import { MessageRequestResponseSource } from '../types/MessageRequestResponseEvent.js'; import type { ChatFolder, ChatFolderId } from '../types/ChatFolder.js'; import { @@ -126,6 +126,8 @@ import { generateNotificationProfileId, normalizeNotificationProfileId, } from '../types/NotificationProfile-node.js'; +import { itemStorage } from '../textsecure/Storage.js'; +import { onHasStoriesDisabledChange } from '../textsecure/WebAPI.js'; const { isEqual } = lodash; @@ -404,7 +406,7 @@ export function toAccountRecord( if (conversation.get('profileFamilyName')) { accountRecord.familyName = conversation.get('profileFamilyName') || ''; } - const avatarUrl = window.storage.get('avatarUrl'); + const avatarUrl = itemStorage.get('avatarUrl'); if (avatarUrl !== undefined) { accountRecord.avatarUrlPath = avatarUrl; } @@ -421,14 +423,12 @@ export function toAccountRecord( accountRecord.typingIndicators = getTypingIndicatorSetting(); accountRecord.linkPreviews = getLinkPreviewSetting(); - const preferContactAvatars = window.storage.get('preferContactAvatars'); + const preferContactAvatars = itemStorage.get('preferContactAvatars'); if (preferContactAvatars !== undefined) { accountRecord.preferContactAvatars = Boolean(preferContactAvatars); } - const rawPreferredReactionEmoji = window.storage.get( - 'preferredReactionEmoji' - ); + const rawPreferredReactionEmoji = itemStorage.get('preferredReactionEmoji'); if (preferredReactionEmoji.canBeSynced(rawPreferredReactionEmoji)) { accountRecord.preferredReactionEmoji = rawPreferredReactionEmoji; } @@ -441,7 +441,7 @@ export function toAccountRecord( const PHONE_NUMBER_SHARING_MODE_ENUM = Proto.AccountRecord.PhoneNumberSharingMode; const phoneNumberSharingMode = parsePhoneNumberSharingMode( - window.storage.get('phoneNumberSharingMode') + itemStorage.get('phoneNumberSharingMode') ); switch (phoneNumberSharingMode) { case PhoneNumberSharingMode.Everybody: @@ -458,7 +458,7 @@ export function toAccountRecord( } const phoneNumberDiscoverability = parsePhoneNumberDiscoverability( - window.storage.get('phoneNumberDiscoverability') + itemStorage.get('phoneNumberDiscoverability') ); switch (phoneNumberDiscoverability) { case PhoneNumberDiscoverability.Discoverable: @@ -471,7 +471,7 @@ export function toAccountRecord( throw missingCaseError(phoneNumberDiscoverability); } - const pinnedConversations = window.storage + const pinnedConversations = itemStorage .get('pinnedConversationIds', new Array()) .map(id => { const pinnedConversation = window.ConversationController.get(id); @@ -530,15 +530,15 @@ export function toAccountRecord( accountRecord.pinnedConversations = pinnedConversations; - const subscriberId = window.storage.get('subscriberId'); + const subscriberId = itemStorage.get('subscriberId'); if (Bytes.isNotEmpty(subscriberId)) { accountRecord.donorSubscriberId = subscriberId; } - const subscriberCurrencyCode = window.storage.get('subscriberCurrencyCode'); + const subscriberCurrencyCode = itemStorage.get('subscriberCurrencyCode'); if (typeof subscriberCurrencyCode === 'string') { accountRecord.donorSubscriberCurrencyCode = subscriberCurrencyCode; } - const donorSubscriptionManuallyCanceled = window.storage.get( + const donorSubscriptionManuallyCanceled = itemStorage.get( 'donorSubscriptionManuallyCancelled' ); if (typeof donorSubscriptionManuallyCanceled === 'boolean') { @@ -547,33 +547,31 @@ export function toAccountRecord( } accountRecord.backupSubscriberData = generateBackupsSubscriberData(); - const backupTier = window.storage.get('backupTier'); + const backupTier = itemStorage.get('backupTier'); if (backupTier) { accountRecord.backupTier = Long.fromNumber(backupTier); } - const displayBadgesOnProfile = window.storage.get('displayBadgesOnProfile'); + const displayBadgesOnProfile = itemStorage.get('displayBadgesOnProfile'); if (displayBadgesOnProfile !== undefined) { accountRecord.displayBadgesOnProfile = displayBadgesOnProfile; } - const keepMutedChatsArchived = window.storage.get('keepMutedChatsArchived'); + const keepMutedChatsArchived = itemStorage.get('keepMutedChatsArchived'); if (keepMutedChatsArchived !== undefined) { accountRecord.keepMutedChatsArchived = keepMutedChatsArchived; } - const hasSetMyStoriesPrivacy = window.storage.get('hasSetMyStoriesPrivacy'); + const hasSetMyStoriesPrivacy = itemStorage.get('hasSetMyStoriesPrivacy'); if (hasSetMyStoriesPrivacy !== undefined) { accountRecord.hasSetMyStoriesPrivacy = hasSetMyStoriesPrivacy; } - const hasViewedOnboardingStory = window.storage.get( - 'hasViewedOnboardingStory' - ); + const hasViewedOnboardingStory = itemStorage.get('hasViewedOnboardingStory'); if (hasViewedOnboardingStory !== undefined) { accountRecord.hasViewedOnboardingStory = hasViewedOnboardingStory; } - const hasCompletedUsernameOnboarding = window.storage.get( + const hasCompletedUsernameOnboarding = itemStorage.get( 'hasCompletedUsernameOnboarding' ); if (hasCompletedUsernameOnboarding !== undefined) { @@ -581,7 +579,7 @@ export function toAccountRecord( hasCompletedUsernameOnboarding; } - const hasSeenGroupStoryEducationSheet = window.storage.get( + const hasSeenGroupStoryEducationSheet = itemStorage.get( 'hasSeenGroupStoryEducationSheet' ); if (hasSeenGroupStoryEducationSheet !== undefined) { @@ -589,12 +587,10 @@ export function toAccountRecord( hasSeenGroupStoryEducationSheet; } - const hasStoriesDisabled = window.storage.get('hasStoriesDisabled'); + const hasStoriesDisabled = itemStorage.get('hasStoriesDisabled'); accountRecord.storiesDisabled = hasStoriesDisabled === true; - const storyViewReceiptsEnabled = window.storage.get( - 'storyViewReceiptsEnabled' - ); + const storyViewReceiptsEnabled = itemStorage.get('storyViewReceiptsEnabled'); if (storyViewReceiptsEnabled !== undefined) { accountRecord.storyViewReceiptsEnabled = storyViewReceiptsEnabled ? Proto.OptionalBool.ENABLED @@ -605,8 +601,8 @@ export function toAccountRecord( // Username link { - const color = window.storage.get('usernameLinkColor'); - const linkData = window.storage.get('usernameLink'); + const color = itemStorage.get('usernameLinkColor'); + const linkData = itemStorage.get('usernameLink'); if (linkData?.entropy.length && linkData?.serverId.length) { accountRecord.usernameLink = { @@ -626,8 +622,8 @@ export function toAccountRecord( notificationProfileSyncDisabled; const override = notificationProfileSyncDisabled - ? window.storage.get('notificationProfileOverrideFromPrimary') - : window.storage.get('notificationProfileOverride'); + ? itemStorage.get('notificationProfileOverrideFromPrimary') + : itemStorage.get('notificationProfileOverride'); if (override?.disabledAtMs && override?.disabledAtMs > 0) { const overrideProto = @@ -1358,7 +1354,7 @@ export async function mergeGroupV2Record( }) ); } else { - const isFirstSync = !window.storage.get('storageFetchComplete'); + const isFirstSync = !itemStorage.get('storageFetchComplete'); const dropInitialJoinMessage = isFirstSync; // We don't await this because this could take a very long time, waiting for queues to @@ -1420,7 +1416,7 @@ export async function mergeContactRecord( } if ( - window.storage.user.getOurServiceIdKind(serviceId) !== ServiceIdKind.Unknown + itemStorage.user.getOurServiceIdKind(serviceId) !== ServiceIdKind.Unknown ) { return { shouldDrop: true, details: ['our own uuid'] }; } @@ -1652,23 +1648,23 @@ export async function mergeAccountRecord( const updatedConversations = new Array(); - await window.storage.put('read-receipt-setting', Boolean(readReceipts)); + await itemStorage.put('read-receipt-setting', Boolean(readReceipts)); if (typeof sealedSenderIndicators === 'boolean') { - await window.storage.put('sealedSenderIndicators', sealedSenderIndicators); + await itemStorage.put('sealedSenderIndicators', sealedSenderIndicators); } if (typeof typingIndicators === 'boolean') { - await window.storage.put('typingIndicators', typingIndicators); + await itemStorage.put('typingIndicators', typingIndicators); } if (typeof linkPreviews === 'boolean') { - await window.storage.put('linkPreviews', linkPreviews); + await itemStorage.put('linkPreviews', linkPreviews); } if (typeof preferContactAvatars === 'boolean') { - const previous = window.storage.get('preferContactAvatars'); - await window.storage.put('preferContactAvatars', preferContactAvatars); + const previous = itemStorage.get('preferContactAvatars'); + await itemStorage.put('preferContactAvatars', preferContactAvatars); if (Boolean(previous) !== Boolean(preferContactAvatars)) { await window.ConversationController.forceRerender(); @@ -1677,7 +1673,7 @@ export async function mergeAccountRecord( if (preferredReactionEmoji.canBeSynced(rawPreferredReactionEmoji)) { const localPreferredReactionEmoji = - window.storage.get('preferredReactionEmoji') || []; + itemStorage.get('preferredReactionEmoji') || []; if (!isEqual(localPreferredReactionEmoji, rawPreferredReactionEmoji)) { log.warn( 'storageService: remote and local preferredReactionEmoji do not match', @@ -1685,10 +1681,7 @@ export async function mergeAccountRecord( rawPreferredReactionEmoji.length ); } - await window.storage.put( - 'preferredReactionEmoji', - rawPreferredReactionEmoji - ); + await itemStorage.put('preferredReactionEmoji', rawPreferredReactionEmoji); } void setUniversalExpireTimer( @@ -1716,7 +1709,7 @@ export async function mergeAccountRecord( phoneNumberSharingModeToStore = PhoneNumberSharingMode.Everybody; break; } - await window.storage.put( + await itemStorage.put( 'phoneNumberSharingMode', phoneNumberSharingModeToStore ); @@ -1724,7 +1717,7 @@ export async function mergeAccountRecord( const discoverability = unlistedPhoneNumber ? PhoneNumberDiscoverability.NotDiscoverable : PhoneNumberDiscoverability.Discoverable; - await window.storage.put('phoneNumberDiscoverability', discoverability); + await itemStorage.put('phoneNumberDiscoverability', discoverability); if (profileKey && profileKey.byteLength > 0) { void ourProfileKeyService.set(profileKey); @@ -1740,7 +1733,7 @@ export async function mergeAccountRecord( convo.get('id') ); - const missingStoragePinnedConversationIds = window.storage + const missingStoragePinnedConversationIds = itemStorage .get('pinnedConversationIds', new Array()) .filter(id => !modelPinnedConversationIds.includes(id)); @@ -1835,23 +1828,23 @@ export async function mergeAccountRecord( updatedConversations.push(convo); }); - await window.storage.put( + await itemStorage.put( 'pinnedConversationIds', remotelyPinnedConversationIds ); } if (Bytes.isNotEmpty(donorSubscriberId)) { - await window.storage.put('subscriberId', donorSubscriberId); + await itemStorage.put('subscriberId', donorSubscriberId); } if (typeof donorSubscriberCurrencyCode === 'string') { - await window.storage.put( + await itemStorage.put( 'subscriberCurrencyCode', donorSubscriberCurrencyCode ); } if (donorSubscriptionManuallyCancelled != null) { - await window.storage.put( + await itemStorage.put( 'donorSubscriptionManuallyCancelled', donorSubscriptionManuallyCancelled ); @@ -1860,21 +1853,21 @@ export async function mergeAccountRecord( await saveBackupsSubscriberData(backupSubscriberData); await saveBackupTier(backupTier?.toNumber()); - await window.storage.put( + await itemStorage.put( 'displayBadgesOnProfile', Boolean(displayBadgesOnProfile) ); - await window.storage.put( + await itemStorage.put( 'keepMutedChatsArchived', Boolean(keepMutedChatsArchived) ); - await window.storage.put( + await itemStorage.put( 'hasSetMyStoriesPrivacy', Boolean(hasSetMyStoriesPrivacy) ); { const hasViewedOnboardingStoryBool = Boolean(hasViewedOnboardingStory); - await window.storage.put( + await itemStorage.put( 'hasViewedOnboardingStory', hasViewedOnboardingStoryBool ); @@ -1888,7 +1881,7 @@ export async function mergeAccountRecord( const hasCompletedUsernameOnboardingBool = Boolean( hasCompletedUsernameOnboarding ); - await window.storage.put( + await itemStorage.put( 'hasCompletedUsernameOnboarding', hasCompletedUsernameOnboardingBool ); @@ -1897,23 +1890,23 @@ export async function mergeAccountRecord( const hasCompletedUsernameOnboardingBool = Boolean( hasSeenGroupStoryEducationSheet ); - await window.storage.put( + await itemStorage.put( 'hasSeenGroupStoryEducationSheet', hasCompletedUsernameOnboardingBool ); } { const hasStoriesDisabled = Boolean(storiesDisabled); - await window.storage.put('hasStoriesDisabled', hasStoriesDisabled); - window.textsecure.server?.onHasStoriesDisabledChange(hasStoriesDisabled); + await itemStorage.put('hasStoriesDisabled', hasStoriesDisabled); + onHasStoriesDisabledChange(hasStoriesDisabled); } switch (storyViewReceiptsEnabled) { case Proto.OptionalBool.ENABLED: - await window.storage.put('storyViewReceiptsEnabled', true); + await itemStorage.put('storyViewReceiptsEnabled', true); break; case Proto.OptionalBool.DISABLED: - await window.storage.put('storyViewReceiptsEnabled', false); + await itemStorage.put('storyViewReceiptsEnabled', false); break; case Proto.OptionalBool.UNSET: default: @@ -1922,33 +1915,33 @@ export async function mergeAccountRecord( } if (usernameLink?.entropy?.length && usernameLink?.serverId?.length) { - const oldLink = window.storage.get('usernameLink'); + const oldLink = itemStorage.get('usernameLink'); if ( - window.storage.get('usernameLinkCorrupted') && + itemStorage.get('usernameLinkCorrupted') && (!oldLink || !Bytes.areEqual(usernameLink.entropy, oldLink.entropy) || !Bytes.areEqual(usernameLink.serverId, oldLink.serverId)) ) { details.push('clearing username link corruption'); - await window.storage.remove('usernameLinkCorrupted'); + await itemStorage.remove('usernameLinkCorrupted'); } await Promise.all([ usernameLink.color && - window.storage.put('usernameLinkColor', usernameLink.color), - window.storage.put('usernameLink', { + itemStorage.put('usernameLinkColor', usernameLink.color), + itemStorage.put('usernameLink', { entropy: usernameLink.entropy, serverId: usernameLink.serverId, }), ]); } else { await Promise.all([ - window.storage.remove('usernameLinkColor'), - window.storage.remove('usernameLink'), + itemStorage.remove('usernameLinkColor'), + itemStorage.remove('usernameLink'), ]); } - const previousSyncDisabled = window.storage.get( + const previousSyncDisabled = itemStorage.get( 'notificationProfileSyncDisabled', false ); @@ -1992,7 +1985,7 @@ export async function mergeAccountRecord( } if (notificationProfileSyncDisabled) { - await window.storage.put( + await itemStorage.put( 'notificationProfileOverrideFromPrimary', overrideToSave ); @@ -2007,11 +2000,11 @@ export async function mergeAccountRecord( const oldStorageVersion = conversation.get('storageVersion'); if ( - window.storage.get('usernameCorrupted') && + itemStorage.get('usernameCorrupted') && username !== conversation.get('username') ) { details.push('clearing username corruption'); - await window.storage.remove('usernameCorrupted'); + await itemStorage.remove('usernameCorrupted'); } conversation.set({ @@ -2035,7 +2028,7 @@ export async function mergeAccountRecord( avatarUrl, decryptionKey: profileKey, }); - await window.storage.put('avatarUrl', avatarUrl); + await itemStorage.put('avatarUrl', avatarUrl); } applyAvatarColor(conversation, accountRecord.avatarColor); @@ -2692,7 +2685,7 @@ export function prepareForDisabledNotificationProfileSync(): { const logId = 'prepareForDisabledNotificationProfileSync'; const state = window.reduxStore.getState(); const { profiles } = state.notificationProfiles; - let newOverride: NotificationProfileOverride | undefined = window.storage.get( + let newOverride: NotificationProfileOverride | undefined = itemStorage.get( 'notificationProfileOverride' ); @@ -2743,7 +2736,7 @@ export function prepareForEnabledNotificationProfileSync(): { const logId = 'prepareForEnabledNotificationProfileSync'; const state = window.reduxStore.getState(); const { profiles } = state.notificationProfiles; - let newOverride: NotificationProfileOverride | undefined = window.storage.get( + let newOverride: NotificationProfileOverride | undefined = itemStorage.get( 'notificationProfileOverride' ); diff --git a/ts/services/storyLoader.ts b/ts/services/storyLoader.ts index af4b9eda15..6ea3cae32d 100644 --- a/ts/services/storyLoader.ts +++ b/ts/services/storyLoader.ts @@ -18,6 +18,7 @@ import { strictAssert } from '../util/assert.js'; import { dropNull } from '../util/dropNull.js'; import { DurationInSeconds } from '../util/durations/index.js'; import { SIGNAL_ACI } from '../types/SignalConversation.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { pick } = lodash; @@ -94,7 +95,7 @@ export function getStoryDataFromMessageAttributes( } else { log.error('getStoryDataFromMessageAttributes: undefined sourceDevice'); // storage user.getDevice() should always produce a value after registration - const ourDeviceId = window.storage.user.getDeviceId() ?? -1; + const ourDeviceId = itemStorage.user.getDeviceId() ?? -1; if (message.type === 'outgoing') { sourceDevice = ourDeviceId; } else if (message.type === 'incoming') { diff --git a/ts/services/username.ts b/ts/services/username.ts index 4b471b6a22..0cf843805d 100644 --- a/ts/services/username.ts +++ b/ts/services/username.ts @@ -22,11 +22,19 @@ import { } from '../types/Username.js'; import * as Errors from '../types/errors.js'; import { createLogger } from '../logging/log.js'; -import MessageSender from '../textsecure/SendMessage.js'; +import { MessageSender } from '../textsecure/SendMessage.js'; +import { + reserveUsername as doReserveUsername, + replaceUsernameLink, + confirmUsername as doConfirmUsername, + deleteUsername as doDeleteUsername, + resolveUsernameLink, +} from '../textsecure/WebAPI.js'; import { HTTPError } from '../types/HTTPError.js'; import { findRetryAfterTimeFromError } from '../jobs/helpers/findRetryAfterTimeFromError.js'; import * as Bytes from '../Bytes.js'; import { storageServiceUploadJob } from './storage.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('username'); @@ -64,11 +72,6 @@ export type ReserveUsernameResultType = Readonly< export async function reserveUsername( options: ReserveUsernameOptionsType ): Promise { - const { server } = window.textsecure; - if (!server) { - throw new Error('server interface is not available!'); - } - const { nickname, customDiscriminator, previousUsername, abortSignal } = options; @@ -114,7 +117,7 @@ export async function reserveUsername( const hashes = candidates.map(username => usernames.hash(username)); - const { usernameHash } = await server.reserveUsername({ + const { usernameHash } = await doReserveUsername({ hashes, abortSignal, }); @@ -229,13 +232,8 @@ export async function confirmUsername( reservation: UsernameReservationType, abortSignal?: AbortSignal ): Promise { - const { server } = window.textsecure; - if (!server) { - throw new Error('server interface is not available!'); - } - const { previousUsername, username } = reservation; - const previousLink = window.storage.get('usernameLink'); + const previousLink = itemStorage.get('usernameLink'); const me = window.ConversationController.getOurConversationOrThrow(); @@ -249,10 +247,10 @@ export async function confirmUsername( 'username hash mismatch' ); - const wasCorrupted = window.storage.get('usernameCorrupted'); + const wasCorrupted = itemStorage.get('usernameCorrupted'); try { - await window.storage.remove('usernameLink'); + await itemStorage.remove('usernameLink'); let serverIdString: string; let entropy: Uint8Array; @@ -265,11 +263,10 @@ export async function confirmUsername( ); ({ entropy } = updatedLink); - ({ usernameLinkHandle: serverIdString } = - await server.replaceUsernameLink({ - encryptedUsername: updatedLink.encryptedUsername, - keepLinkHandle: true, - })); + ({ usernameLinkHandle: serverIdString } = await replaceUsernameLink({ + encryptedUsername: updatedLink.encryptedUsername, + keepLinkHandle: true, + })); } else { log.info('confirmUsername: confirming and replacing link'); @@ -278,7 +275,7 @@ export async function confirmUsername( const proof = usernames.generateProof(username); - ({ usernameLinkHandle: serverIdString } = await server.confirmUsername({ + ({ usernameLinkHandle: serverIdString } = await doConfirmUsername({ hash, proof, encryptedUsername: newLink.encryptedUsername, @@ -286,14 +283,14 @@ export async function confirmUsername( })); } - await window.storage.put('usernameLink', { + await itemStorage.put('usernameLink', { entropy, serverId: uuidToBytes(serverIdString), }); await updateUsernameAndSyncProfile(username); - await window.storage.remove('usernameCorrupted'); - await window.storage.remove('usernameLinkCorrupted'); + await itemStorage.remove('usernameCorrupted'); + await itemStorage.remove('usernameLinkCorrupted'); } catch (error) { if (error instanceof HTTPError) { if (error.code === 413 || error.code === 429) { @@ -320,29 +317,19 @@ export async function deleteUsername( previousUsername: string | undefined, abortSignal?: AbortSignal ): Promise { - const { server } = window.textsecure; - if (!server) { - throw new Error('server interface is not available!'); - } - const me = window.ConversationController.getOurConversationOrThrow(); if (me.get('username') !== previousUsername) { throw new Error('Username has changed on another device'); } - await window.storage.remove('usernameLink'); - await server.deleteUsername(abortSignal); - await window.storage.remove('usernameCorrupted'); + await itemStorage.remove('usernameLink'); + await doDeleteUsername(abortSignal); + await itemStorage.remove('usernameCorrupted'); await updateUsernameAndSyncProfile(undefined); } export async function resetLink(username: string): Promise { - const { server } = window.textsecure; - if (!server) { - throw new Error('server interface is not available!'); - } - const me = window.ConversationController.getOurConversationOrThrow(); if (me.get('username') !== username) { @@ -351,19 +338,18 @@ export async function resetLink(username: string): Promise { const { entropy, encryptedUsername } = usernames.createUsernameLink(username); - await window.storage.remove('usernameLink'); + await itemStorage.remove('usernameLink'); - const { usernameLinkHandle: serverIdString } = - await server.replaceUsernameLink({ - encryptedUsername, - keepLinkHandle: false, - }); + const { usernameLinkHandle: serverIdString } = await replaceUsernameLink({ + encryptedUsername, + keepLinkHandle: false, + }); - await window.storage.put('usernameLink', { + await itemStorage.put('usernameLink', { entropy, serverId: uuidToBytes(serverIdString), }); - await window.storage.remove('usernameLinkCorrupted'); + await itemStorage.remove('usernameLinkCorrupted'); me.captureChange('usernameLink'); storageServiceUploadJob({ reason: 'resetLink' }); @@ -390,18 +376,11 @@ export async function resolveUsernameByLink({ entropy, serverId: serverIdBytes, }: ResolveUsernameByLinkOptionsType): Promise { - const { server } = window.textsecure; - if (!server) { - throw new Error('server interface is not available!'); - } - const serverId = bytesToUuid(serverIdBytes); strictAssert(serverId, 'Failed to re-encode server id as uuid'); - strictAssert(window.textsecure.server, 'WebAPI must be available'); try { - const { usernameLinkEncryptedValue } = - await server.resolveUsernameLink(serverId); + const { usernameLinkEncryptedValue } = await resolveUsernameLink(serverId); return usernames.decryptUsernameLink({ entropy, diff --git a/ts/services/usernameIntegrity.ts b/ts/services/usernameIntegrity.ts index 4a3e0d5155..5403f7f867 100644 --- a/ts/services/usernameIntegrity.ts +++ b/ts/services/usernameIntegrity.ts @@ -5,7 +5,7 @@ import pTimeout from 'p-timeout'; import { usernames } from '@signalapp/libsignal-client'; import * as Errors from '../types/errors.js'; -import { strictAssert } from '../util/assert.js'; +import { whoami } from '../textsecure/WebAPI.js'; import { isDone as isRegistrationDone } from '../util/registration.js'; import { getConversation } from '../util/getConversation.js'; import { MINUTE, DAY } from '../util/durations/index.js'; @@ -20,6 +20,7 @@ import { createLogger } from '../logging/log.js'; import * as Bytes from '../Bytes.js'; import { runStorageServiceSyncJob } from './storage.js'; import { writeProfile } from './writeProfile.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('usernameIntegrity'); @@ -42,10 +43,7 @@ class UsernameIntegrityService { } #scheduleCheck(): void { - const lastCheckTimestamp = window.storage.get( - 'usernameLastIntegrityCheck', - 0 - ); + const lastCheckTimestamp = itemStorage.get('usernameLastIntegrityCheck', 0); const delay = Math.max(0, lastCheckTimestamp + CHECK_INTERVAL - Date.now()); if (delay === 0) { log.info('running the check immediately'); @@ -60,7 +58,7 @@ class UsernameIntegrityService { try { await storageJobQueue(() => this.#check()); this.#backOff.reset(); - await window.storage.put('usernameLastIntegrityCheck', Date.now()); + await itemStorage.put('usernameLastIntegrityCheck', Date.now()); this.#scheduleCheck(); } catch (error) { @@ -90,27 +88,20 @@ class UsernameIntegrityService { return; } - const { server } = window.textsecure; - if (!server) { - log.info('server interface is not available'); - return; - } - - strictAssert(window.textsecure.server, 'WebAPI must be available'); const { usernameHash: remoteHash, usernameLinkHandle: remoteLink } = - await server.whoami(); + await whoami(); let failed = false; if (remoteHash !== Bytes.toBase64url(usernames.hash(username))) { log.error('remote username mismatch'); - await window.storage.put('usernameCorrupted', true); + await itemStorage.put('usernameCorrupted', true); failed = true; // Intentional fall-through } - const link = window.storage.get('usernameLink'); + const link = itemStorage.get('usernameLink'); if (!link) { log.info('no username link'); return; @@ -118,7 +109,7 @@ class UsernameIntegrityService { if (remoteLink !== bytesToUuid(link.serverId)) { log.error('username link mismatch'); - await window.storage.put('usernameLinkCorrupted', true); + await itemStorage.put('usernameLinkCorrupted', true); failed = true; } diff --git a/ts/services/writeProfile.ts b/ts/services/writeProfile.ts index a8e43500e3..a40d3d0410 100644 --- a/ts/services/writeProfile.ts +++ b/ts/services/writeProfile.ts @@ -3,6 +3,7 @@ import { DataWriter } from '../sql/Client.js'; import type { ConversationType } from '../state/ducks/conversations.js'; +import { putProfile, uploadAvatar } from '../textsecure/WebAPI.js'; import * as Errors from '../types/errors.js'; import { createLogger } from '../logging/log.js'; import { computeHash } from '../Crypto.js'; @@ -17,7 +18,8 @@ import type { AvatarUpdateOptionsType, AvatarUpdateType, } from '../types/Avatar.js'; -import MessageSender from '../textsecure/SendMessage.js'; +import { MessageSender } from '../textsecure/SendMessage.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('writeProfile'); @@ -25,11 +27,6 @@ export async function writeProfile( conversation: ConversationType, options: AvatarUpdateOptionsType ): Promise { - const { server } = window.textsecure; - if (!server) { - throw new Error('server is not available!'); - } - // Before we write anything we request the user's profile so that we can // have an up-to-date paymentAddress to be able to include it when we write const model = window.ConversationController.get(conversation.id); @@ -83,7 +80,7 @@ export async function writeProfile( conversation, avatarUpdate ); - const avatarRequestHeaders = await server.putProfile(profileData); + const avatarRequestHeaders = await putProfile(profileData); // Upload the avatar if provided // delete existing files on disk if avatar has been removed @@ -105,7 +102,7 @@ export async function writeProfile( newAvatar ) { log.info('uploading new avatar'); - const avatarUrl = await server.uploadAvatar( + const avatarUrl = await uploadAvatar( avatarRequestHeaders, encryptedAvatarData ); @@ -125,12 +122,12 @@ export async function writeProfile( }; } - await window.storage.put('avatarUrl', avatarUrl); + await itemStorage.put('avatarUrl', avatarUrl); } else if (rawAvatarPath) { log.info('removing avatar'); await Promise.all([ window.Signal.Migrations.deleteAttachmentData(rawAvatarPath), - window.storage.put('avatarUrl', undefined), + itemStorage.put('avatarUrl', undefined), ]); maybeProfileAvatarUpdate = { profileAvatar: undefined }; diff --git a/ts/shims/deleteAllData.ts b/ts/shims/deleteAllData.ts index 8405921204..478ac6711c 100644 --- a/ts/shims/deleteAllData.ts +++ b/ts/shims/deleteAllData.ts @@ -5,6 +5,7 @@ import { createLogger } from '../logging/log.js'; import { DataWriter } from '../sql/Client.js'; import { deleteAllLogs } from '../util/deleteAllLogs.js'; import * as Errors from '../types/errors.js'; +import { unlink } from '../textsecure/WebAPI.js'; const log = createLogger('deleteAllData'); @@ -12,7 +13,7 @@ export async function deleteAllData(): Promise { try { // This might fail if websocket closes before we receive the response, while // still unlinking the device on the server. - await window.textsecure.server?.unlink(); + await unlink(); } catch (error) { log.error( 'Something went wrong unlinking device:', diff --git a/ts/shims/storage.ts b/ts/shims/storage.ts index a14a865a71..2050fc292b 100644 --- a/ts/shims/storage.ts +++ b/ts/shims/storage.ts @@ -2,15 +2,16 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { StorageAccessType } from '../types/Storage.d.ts'; +import { itemStorage } from '../textsecure/Storage.js'; -// Matching window.storage.put API +// Matching storage.put API export async function put( key: K, value: StorageAccessType[K] ): Promise { - await window.storage.put(key, value); + await itemStorage.put(key, value); } export async function remove(key: keyof StorageAccessType): Promise { - await window.storage.remove(key); + await itemStorage.remove(key); } diff --git a/ts/shims/textsecure.ts b/ts/shims/textsecure.ts index d32f03bf71..83b08cd22b 100644 --- a/ts/shims/textsecure.ts +++ b/ts/shims/textsecure.ts @@ -4,7 +4,7 @@ import { createLogger } from '../logging/log.js'; import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue.js'; import * as Errors from '../types/errors.js'; -import MessageSender from '../textsecure/SendMessage.js'; +import { MessageSender } from '../textsecure/SendMessage.js'; const log = createLogger('textsecure'); diff --git a/ts/state/ducks/accounts.ts b/ts/state/ducks/accounts.ts index 3903c492d2..23de639b88 100644 --- a/ts/state/ducks/accounts.ts +++ b/ts/state/ducks/accounts.ts @@ -5,6 +5,7 @@ import type { ThunkAction } from 'redux-thunk'; import type { ReadonlyDeep } from 'type-fest'; import * as Errors from '../../types/errors.js'; +import { cdsLookup } from '../../textsecure/WebAPI.js'; import { createLogger } from '../../logging/log.js'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.js'; @@ -54,15 +55,6 @@ function checkForAccount( AccountUpdateActionType | NoopActionType > { return async (dispatch, getState) => { - const { server } = window.textsecure; - if (!server) { - dispatch({ - type: 'NOOP', - payload: null, - }); - return; - } - const conversation = window.ConversationController.get(phoneNumber); if (conversation && conversation.getServiceId()) { log.info(`checkForAccount: found ${phoneNumber} in existing contacts`); @@ -96,7 +88,7 @@ function checkForAccount( log.info(`checkForAccount: looking ${phoneNumber} up on server`); try { const { entries: serviceIdLookup, transformedE164s } = - await getServiceIdsForE164s(server, [phoneNumber]); + await getServiceIdsForE164s(cdsLookup, [phoneNumber]); const phoneNumberToUse = transformedE164s.get(phoneNumber) ?? phoneNumber; const maybePair = serviceIdLookup.get(phoneNumberToUse); diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index cb23ffc0b2..4d86c3e7b5 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -112,6 +112,8 @@ import { import { storageServiceUploadJob } from '../../services/storage.js'; import { CallLinkFinalizeDeleteManager } from '../../jobs/CallLinkFinalizeDeleteManager.js'; import { callLinkRefreshJobQueue } from '../../jobs/callLinkRefreshJobQueue.js'; +import { isOnline } from '../../textsecure/WebAPI.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { omit } = lodash; @@ -569,12 +571,7 @@ const doGroupCallPeek = ({ // If we peek right after receiving the message, we may get outdated information. // This is most noticeable when someone leaves. We add a delay and then make sure // to only be peeking once. - const { server } = window.textsecure; - if (!server) { - log.error('doGroupCallPeek: no textsecure server'); - return; - } - await Promise.all([sleep(1000), waitForOnline()]); + await Promise.all([sleep(1000), waitForOnline({ server: { isOnline } })]); let peekInfo = null; try { @@ -2164,7 +2161,7 @@ function onOutgoingVideoCallInConversation( const call = getOwn(getState().calling.callsByConversation, conversationId); // Technically not necessary, but isAnybodyElseInGroupCall requires it - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const isOngoingGroupCall = call && ourAci && diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index c7e136f026..f8d2500923 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -69,7 +69,7 @@ import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessa import { writeDraftAttachment } from '../../util/writeDraftAttachment.js'; import { getMessageById } from '../../messages/getMessageById.js'; import { canReply, isNormalBubble } from '../selectors/message.js'; -import { getAuthorId } from '../../messages/helpers.js'; +import { getAuthorId } from '../../messages/sources.js'; import { getConversationSelector } from '../selectors/conversations.js'; import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend.js'; import { useBoundActions } from '../../hooks/useBoundActions.js'; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index c9c01c5739..2f2646e8b8 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -188,7 +188,7 @@ import { getMessageIdForLogging, } from '../../util/idForLogging.js'; import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue.js'; -import MessageSender from '../../textsecure/SendMessage.js'; +import { MessageSender } from '../../textsecure/SendMessage.js'; import { AttachmentDownloadManager } from '../../jobs/AttachmentDownloadManager.js'; import type { DeleteForMeSyncEventData, @@ -224,6 +224,7 @@ import { } from '../../types/ChatFolder.js'; import { getCurrentChatFolders } from '../selectors/chatFolders.js'; import { isConversationUnread } from '../../util/isConversationUnread.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { chunk, @@ -1837,7 +1838,7 @@ function setPinned( } if (value) { - const pinnedConversationIds = window.storage.get( + const pinnedConversationIds = itemStorage.get( 'pinnedConversationIds', new Array() ); @@ -2176,7 +2177,7 @@ export const markViewed = (messageId: string): void => { ); } else { // Use our own ACI for syncing viewed state of an outgoing message. - senderAci = window.textsecure.storage.user.getCheckedAci(); + senderAci = itemStorage.user.getCheckedAci(); } drop( @@ -3875,9 +3876,7 @@ function blockAndReportSpam( } else { try { await singleProtoJobQueue.add( - MessageSender.getBlockSync( - window.textsecure.storage.blocked.getBlockedData() - ) + MessageSender.getBlockSync(itemStorage.blocked.getBlockedData()) ); } catch (error) { log.error( @@ -3928,9 +3927,7 @@ function acceptConversation( try { await singleProtoJobQueue.add( - MessageSender.getBlockSync( - window.textsecure.storage.blocked.getBlockedData() - ) + MessageSender.getBlockSync(itemStorage.blocked.getBlockedData()) ); } catch (error) { log.error( @@ -4009,9 +4006,7 @@ function blockConversation( try { await singleProtoJobQueue.add( - MessageSender.getBlockSync( - window.textsecure.storage.blocked.getBlockedData() - ) + MessageSender.getBlockSync(itemStorage.blocked.getBlockedData()) ); } catch (error) { log.error( @@ -4854,7 +4849,7 @@ function onConversationOpened( ); promises.push(conversation.throttledFetchSMSOnlyUUID()); - const ourAci = window.textsecure.storage.user.getAci(); + const ourAci = itemStorage.user.getAci(); if ( !isGroup(conversation.attributes) || (ourAci && conversation.hasMember(ourAci)) diff --git a/ts/state/ducks/donations.ts b/ts/state/ducks/donations.ts index 6492ff5dd7..97bd6ec272 100644 --- a/ts/state/ducks/donations.ts +++ b/ts/state/ducks/donations.ts @@ -30,6 +30,7 @@ import type { } from '../../types/Donations.js'; import type { BadgeType } from '../../badges/types.js'; import type { StateType as RootStateType } from '../reducer.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const log = createLogger('donations'); @@ -218,10 +219,14 @@ export function applyDonationBadge({ badge, applyBadge, onComplete, + storage = itemStorage, }: { badge: BadgeType | undefined; applyBadge: boolean; onComplete: (error?: Error) => void; + + // Only for testing + storage?: Pick; }): ThunkAction { return async (dispatch, getState) => { const me = getMe(getState()); @@ -302,15 +307,12 @@ export function applyDonationBadge({ newDisplayBadgesOnProfile = false; } - const storageValue = window.storage.get('displayBadgesOnProfile'); + const storageValue = storage.get('displayBadgesOnProfile'); if ( storageValue == null || previousDisplayBadgesOnProfile !== newDisplayBadgesOnProfile ) { - await window.storage.put( - 'displayBadgesOnProfile', - newDisplayBadgesOnProfile - ); + await storage.put('displayBadgesOnProfile', newDisplayBadgesOnProfile); if (previousDisplayBadgesOnProfile !== newDisplayBadgesOnProfile) { storageServiceUploadJob({ reason: 'donation-badge-toggle' }); } diff --git a/ts/state/ducks/installer.ts b/ts/state/ducks/installer.ts index 9af9703b10..b94711a870 100644 --- a/ts/state/ducks/installer.ts +++ b/ts/state/ducks/installer.ts @@ -24,6 +24,7 @@ import { EventKind as ProvisionEventKind, type EnvelopeType as ProvisionEnvelopeType, } from '../../textsecure/Provisioner.js'; +import { getProvisioningResource } from '../../textsecure/WebAPI.js'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.js'; import { useBoundActions } from '../../hooks/useBoundActions.js'; import { createLogger } from '../../logging/log.js'; @@ -174,11 +175,12 @@ function startInstaller(): ThunkAction< 'Unexpected step after START_INSTALLER' ); - const { server } = window.textsecure; - strictAssert(server, 'Expected a server'); - if (!provisioner) { - provisioner = new Provisioner({ server }); + provisioner = new Provisioner({ + server: { + getProvisioningResource, + }, + }); } const cancel = provisioner.subscribe(event => { diff --git a/ts/state/ducks/notificationProfiles.ts b/ts/state/ducks/notificationProfiles.ts index dd8917ce1c..fda65fe720 100644 --- a/ts/state/ducks/notificationProfiles.ts +++ b/ts/state/ducks/notificationProfiles.ts @@ -37,6 +37,7 @@ import type { NotificationProfileType, } from '../../types/NotificationProfile.js'; import type { StateType } from '../reducer.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const log = createLogger('ducks/notificationProfiles'); @@ -149,7 +150,7 @@ export const useNotificationProfilesActions = (): BoundActionCreatorsMapObject< const updateStorageService = debounce( (reason: string, options: { force?: boolean } = {}) => { - const disabled = window.storage.get('notificationProfileSyncDisabled'); + const disabled = itemStorage.get('notificationProfileSyncDisabled'); if (disabled && !options.force) { return; } @@ -223,7 +224,7 @@ function setIsSyncEnabled( return; } - // Because we can't update everything (window.storage and our redux slice), there is + // Because we can't update everything (itemStorage and our redux slice), there is // the risk of a flash of content on the list page when enabling/disabling sync. So // we set this loading flag and show something else until everything is ready. try { @@ -232,14 +233,14 @@ function setIsSyncEnabled( payload: true, }); - await window.storage.put('notificationProfileSyncDisabled', disabled); + await itemStorage.put('notificationProfileSyncDisabled', disabled); if (disabled) { if (!fromStorageService) { - const globalOverride = await window.storage.get( + const globalOverride = await itemStorage.get( 'notificationProfileOverride' ); - await window.storage.put( + await itemStorage.put( 'notificationProfileOverrideFromPrimary', globalOverride ); @@ -254,14 +255,14 @@ function setIsSyncEnabled( newOverride, }, }); - await window.storage.put('notificationProfileOverride', newOverride); + await itemStorage.put('notificationProfileOverride', newOverride); await Promise.all( toAdd.map(async profile => { await DataWriter.createNotificationProfile(profile); }) ); } else { - await window.storage.put( + await itemStorage.put( 'notificationProfileOverrideFromPrimary', undefined ); @@ -275,7 +276,7 @@ function setIsSyncEnabled( newOverride, }, }); - await window.storage.put('notificationProfileOverride', newOverride); + await itemStorage.put('notificationProfileOverride', newOverride); await Promise.all( toRemove.map(async profile => { await DataWriter.deleteNotificationProfileById(profile.id); @@ -338,7 +339,7 @@ function setProfileOverride( endsAtMs, }, }; - await window.storage.put('notificationProfileOverride', newOverride); + await itemStorage.put('notificationProfileOverride', newOverride); dispatch({ type: UPDATE_OVERRIDE, payload: newOverride, @@ -353,7 +354,7 @@ function setProfileOverride( disabledAtMs: Date.now(), enabled: undefined, }; - await window.storage.put('notificationProfileOverride', newOverride); + await itemStorage.put('notificationProfileOverride', newOverride); dispatch({ type: UPDATE_OVERRIDE, payload: newOverride, @@ -390,7 +391,7 @@ function updateOverride( return async dispatch => { const id = payload?.enabled?.profileId; const enabled = payload?.enabled; - await window.storage.put('notificationProfileOverride', payload); + await itemStorage.put('notificationProfileOverride', payload); const logId = `updateOverride/${id ? redactNotificationProfileId(id) : 'undefined'}/enabled=${enabled}`; diff --git a/ts/state/ducks/preferredReactions.ts b/ts/state/ducks/preferredReactions.ts index 83d8fc9bde..c750444825 100644 --- a/ts/state/ducks/preferredReactions.ts +++ b/ts/state/ducks/preferredReactions.ts @@ -17,6 +17,7 @@ import { EmojiSkinTone, getEmojiVariantByParentKeyAndSkinTone, } from '../../components/fun/data/emojis.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { omit } = lodash; @@ -183,10 +184,7 @@ function savePreferredReactions(): ThunkAction< dispatch({ type: SAVE_PREFERRED_REACTIONS_PENDING }); try { - await window.storage.put( - 'preferredReactionEmoji', - draftPreferredReactions - ); + await itemStorage.put('preferredReactionEmoji', draftPreferredReactions); succeeded = true; } catch (err: unknown) { log.warn(Errors.toLogFormat(err)); diff --git a/ts/state/ducks/stories.ts b/ts/state/ducks/stories.ts index 2ded7fd193..5a61fc49fd 100644 --- a/ts/state/ducks/stories.ts +++ b/ts/state/ducks/stories.ts @@ -33,10 +33,10 @@ import { ReadStatus } from '../../messages/MessageReadStatus.js'; import { SendStatus } from '../../messages/MessageSendState.js'; import { SafetyNumberChangeSource } from '../../types/SafetyNumberChangeSource.js'; import { - areStoryViewReceiptsEnabled, StoryViewDirectionType, StoryViewModeType, } from '../../types/Stories.js'; +import { areStoryViewReceiptsEnabled } from '../../util/Settings.js'; import { assertDev, strictAssert } from '../../util/assert.js'; import { drop } from '../../util/drop.js'; import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified.js'; @@ -83,6 +83,7 @@ import { import { ReceiptType } from '../../types/Receipt.js'; import { cleanupMessages } from '../../util/cleanup.js'; import { AttachmentDownloadUrgency } from '../../types/AttachmentDownload.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { isEqual, pick } = lodash; @@ -847,7 +848,7 @@ const getSelectedStoryDataForConversationId = ( const state = getState(); const { stories } = state.stories; - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const storiesByConversationId = stories.filter( item => item.conversationId === conversationId && diff --git a/ts/state/ducks/storyDistributionLists.ts b/ts/state/ducks/storyDistributionLists.ts index fbd0dcd2d1..fc7581f439 100644 --- a/ts/state/ducks/storyDistributionLists.ts +++ b/ts/state/ducks/storyDistributionLists.ts @@ -18,6 +18,7 @@ import { replaceIndex } from '../../util/replaceIndex.js'; import { storageServiceUploadJob } from '../../services/storage.js'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.js'; import { useBoundActions } from '../../hooks/useBoundActions.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { omit } = lodash; @@ -289,7 +290,7 @@ function hideMyStoriesFrom( reason: 'storyDistributionLists/hideMyStoriesFrom', }); - await window.storage.put('hasSetMyStoriesPrivacy', true); + await itemStorage.put('hasSetMyStoriesPrivacy', true); dispatch({ type: HIDE_MY_STORIES_FROM, @@ -336,7 +337,7 @@ function removeMembersFromDistributionList( toRemove = []; // The user has now configured My Stories - await window.storage.put('hasSetMyStoriesPrivacy', true); + await itemStorage.put('hasSetMyStoriesPrivacy', true); } await DataWriter.modifyStoryDistributionWithMembers( @@ -404,7 +405,7 @@ function setMyStoriesToAllSignalConnections(): ThunkAction< storageServiceUploadJob({ reason: 'setMyStoriesToAllSignalConnections' }); } - await window.storage.put('hasSetMyStoriesPrivacy', true); + await itemStorage.put('hasSetMyStoriesPrivacy', true); dispatch({ type: RESET_MY_STORIES, @@ -460,7 +461,7 @@ function updateStoryViewers( storageServiceUploadJob({ reason: 'updateStoryViewers' }); if (listId === MY_STORY_ID) { - await window.storage.put('hasSetMyStoriesPrivacy', true); + await itemStorage.put('hasSetMyStoriesPrivacy', true); } dispatch({ diff --git a/ts/state/ducks/username.ts b/ts/state/ducks/username.ts index 3f24baadf9..6fb8cc8b59 100644 --- a/ts/state/ducks/username.ts +++ b/ts/state/ducks/username.ts @@ -30,6 +30,7 @@ import { ToastType } from '../../types/Toast.js'; import type { ToastActionType } from './toast.js'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.js'; import { useBoundActions } from '../../hooks/useBoundActions.js'; +import { itemStorage } from '../../textsecure/Storage.js'; export type UsernameReservationStateType = ReadonlyDeep<{ state: UsernameReservationState; @@ -324,7 +325,7 @@ function markCompletedUsernameOnboarding(): ThunkAction< never > { return async () => { - await window.storage.put('hasCompletedUsernameOnboarding', true); + await itemStorage.put('hasCompletedUsernameOnboarding', true); const me = window.ConversationController.getOurConversationOrThrow(); me.captureChange('usernameOnboarding'); storageServiceUploadJob({ reason: 'markCompletedUsernameOnboarding' }); @@ -338,7 +339,7 @@ function markCompletedUsernameLinkOnboarding(): ThunkAction< never > { return async () => { - await window.storage.put('hasCompletedUsernameLinkOnboarding', true); + await itemStorage.put('hasCompletedUsernameLinkOnboarding', true); }; } @@ -346,7 +347,7 @@ function setUsernameLinkColor( color: number ): ThunkAction { return async () => { - await window.storage.put('usernameLinkColor', color); + await itemStorage.put('usernameLinkColor', color); const me = window.ConversationController.getOurConversationOrThrow(); me.captureChange('usernameLinkColor'); storageServiceUploadJob({ reason: 'setUsernameLinkColor' }); diff --git a/ts/state/getInitialState.ts b/ts/state/getInitialState.ts index 3298cadb2c..affcc227d7 100644 --- a/ts/state/getInitialState.ts +++ b/ts/state/getInitialState.ts @@ -53,6 +53,7 @@ import type { import type { ThemeType } from '../types/Util.js'; import type { UserStateType } from './ducks/user.js'; import type { ReduxInitData } from './initializeRedux.js'; +import { itemStorage } from '../textsecure/Storage.js'; export function getInitialState( { @@ -74,7 +75,7 @@ export function getInitialState( }: ReduxInitData, existingState?: StateType ): StateType { - const items = window.storage.getItemsState(); + const items = itemStorage.getItemsState(); const baseState: StateType = existingState ?? getEmptyState(); @@ -196,12 +197,12 @@ export function generateUserState({ menuOptions: MenuOptionsType; theme: ThemeType; }): UserStateType { - const ourNumber = window.textsecure.storage.user.getNumber(); - const ourAci = window.textsecure.storage.user.getAci(); - const ourPni = window.textsecure.storage.user.getPni(); + const ourNumber = itemStorage.user.getNumber(); + const ourAci = itemStorage.user.getAci(); + const ourPni = itemStorage.user.getPni(); const ourConversationId = window.ConversationController.getOurConversationId(); - const ourDeviceId = window.textsecure.storage.user.getDeviceId(); + const ourDeviceId = itemStorage.user.getDeviceId(); let osName: 'windows' | 'macos' | 'linux' | undefined; @@ -229,7 +230,7 @@ export function generateUserState({ ourNumber, ourPni, platform: window.platform, - regionCode: window.storage.get('regionCode'), + regionCode: itemStorage.get('regionCode'), stickersPath: window.BasePaths.stickers, tempPath: window.BasePaths.temp, theme, diff --git a/ts/state/selectors/conversations-extra.ts b/ts/state/selectors/conversations-extra.ts index 45075dcce4..64383e6ec8 100644 --- a/ts/state/selectors/conversations-extra.ts +++ b/ts/state/selectors/conversations-extra.ts @@ -11,9 +11,11 @@ import type { ContactsByStory } from '../../components/SafetyNumberChangeDialog. import type { ConversationVerificationData } from '../ducks/conversations.js'; import type { StoryDistributionListDataType } from '../ducks/storyDistributionLists.js'; import type { GetConversationByIdType } from './conversations.js'; +import { isSignalConnection } from '../../util/getSignalConnections.js'; import { isGroup } from '../../util/whatTypeOfConversation.js'; import { + getAllConversations, getConversationSelector, getConversationVerificationData, } from './conversations.js'; @@ -101,3 +103,12 @@ export const getByDistributionListConversationsStoppingSend = createSelector( return conversations; } ); + +// `isSignalConnection` accesses itemStorage to determine which conversation +// is blocked so we can't export it in `conversations.ts` (which is imported +// directly for some components) +export const getAllSignalConnections = createSelector( + getAllConversations, + (conversations): ReturnType => + conversations.filter(isSignalConnection) +); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 8660e3ccf9..eaae9af813 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -36,7 +36,6 @@ import type { AvatarDataType } from '../../types/Avatar.js'; import type { AciString, ServiceIdString } from '../../types/ServiceId.js'; import { normalizeServiceId } from '../../types/ServiceId.js'; import { isInSystemContacts } from '../../util/isInSystemContacts.js'; -import { isSignalConnection } from '../../util/getSignalConnections.js'; import { sortByTitle } from '../../util/sortByTitle.js'; import { DurationInSeconds } from '../../util/durations/index.js'; import { @@ -164,12 +163,6 @@ export const getAllConversations = createSelector( (lookup): Array => Object.values(lookup) ); -export const getAllSignalConnections = createSelector( - getAllConversations, - (conversations): Array => - conversations.filter(isSignalConnection) -); - export const getSafeConversationWithSameTitle = createSelector( getAllConversations, ( diff --git a/ts/state/selectors/items.ts b/ts/state/selectors/items.ts index f884cd332d..c96c2a661c 100644 --- a/ts/state/selectors/items.ts +++ b/ts/state/selectors/items.ts @@ -3,7 +3,6 @@ import { createSelector } from 'reselect'; -import { ITEM_NAME as UNIVERSAL_EXPIRE_TIMER_ITEM } from '../../util/universalExpireTimer.js'; import { innerIsBucketValueEnabled } from '../../RemoteConfig.js'; import type { ConfigKeyType, ConfigMapType } from '../../RemoteConfig.js'; import type { StateType } from '../reducer.js'; @@ -55,7 +54,7 @@ export const getPinnedConversationIds = createSelector( export const getUniversalExpireTimer = createSelector( getItems, (state: ItemsStateType): DurationInSeconds => - DurationInSeconds.fromSeconds(state[UNIVERSAL_EXPIRE_TIMER_ITEM] || 0) + DurationInSeconds.fromSeconds(state.universalExpireTimer || 0) ); export const isRemoteConfigFlagEnabled = ( @@ -263,6 +262,13 @@ export const getShowStickerPickerHint = createSelector( } ); +export const getHasUnidentifiedDeliveryIndicators = createSelector( + getItems, + (state: ItemsStateType): boolean => { + return state.unidentifiedDeliveryIndicators ?? false; + } +); + export const getLocalDeleteWarningShown = createSelector( getItems, (state: ItemsStateType): boolean => diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index f6d1e83549..8f72460f0e 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -90,7 +90,10 @@ import { import { isPermanentlyUndownloadable } from '../../jobs/AttachmentDownloadManager.js'; import { getAccountSelector } from './accounts.js'; -import { getDefaultConversationColor } from './items.js'; +import { + getDefaultConversationColor, + getHasUnidentifiedDeliveryIndicators, +} from './items.js'; import { getConversationSelector, getSelectedMessageIds, @@ -133,13 +136,9 @@ import { createLogger } from '../../logging/log.js'; import { getConversationColorAttributes } from '../../util/getConversationColorAttributes.js'; import { DAY, DurationInSeconds } from '../../util/durations/index.js'; import { getStoryReplyText } from '../../util/getStoryReplyText.js'; -import type { MessageAttributesWithPaymentEvent } from '../../messages/helpers.js'; -import { - isIncoming, - isOutgoing, - messageHasPaymentEvent, - isStory, -} from '../../messages/helpers.js'; +import type { MessageAttributesWithPaymentEvent } from '../../messages/payments.js'; +import { isIncoming, isOutgoing, isStory } from '../../messages/helpers.js'; +import { messageHasPaymentEvent } from '../../messages/payments.js'; import { calculateExpirationTimestamp } from '../../util/expirationTimer.js'; import { isSignalConversation } from '../../util/isSignalConversation.js'; @@ -2270,6 +2269,7 @@ export const getMessageDetails = createSelector( getUserNumber, getSelectedMessageIds, getDefaultConversationColor, + getHasUnidentifiedDeliveryIndicators, ( accountSelector, cachedConversationMemberColorsSelector, @@ -2282,7 +2282,8 @@ export const getMessageDetails = createSelector( ourConversationId, ourNumber, selectedMessageIds, - defaultConversationColor + defaultConversationColor, + hasUnidentifiedDeliveryIndicators ): SmartMessageDetailPropsType | undefined => { if (!message || !ourConversationId) { return; @@ -2363,11 +2364,6 @@ export const getMessageDetails = createSelector( return window.ConversationController.getConversationId(serviceId); }); - const hasUnidentifiedDeliveryIndicators = window.storage.get( - 'unidentifiedDeliveryIndicators', - false - ); - const contacts: ReadonlyArray = conversationIds.map(id => { const errorsForContact = getOwn(errorsGroupedById, id); diff --git a/ts/state/smart/App.tsx b/ts/state/smart/App.tsx index 8fd28f3526..6009496d44 100644 --- a/ts/state/smart/App.tsx +++ b/ts/state/smart/App.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import React, { memo } from 'react'; import { useSelector } from 'react-redux'; +import { requestVerification as doRequestVerification } from '../../textsecure/WebAPI.js'; import type { VerificationTransport } from '../../types/VerificationTransport.js'; import { DataWriter } from '../../sql/Client.js'; import { App } from '../../components/App.js'; @@ -9,7 +10,6 @@ import OS from '../../util/os/osMain.js'; import { getConversation } from '../../util/getConversation.js'; import { getChallengeURL } from '../../challenge.js'; import { writeProfile } from '../../services/writeProfile.js'; -import { strictAssert } from '../../util/assert.js'; import { SmartCallManager } from './CallManager.js'; import { SmartGlobalModalContainer } from './GlobalModalContainer.js'; import { SmartLightbox } from './Lightbox.js'; @@ -73,9 +73,7 @@ function requestVerification( captcha: string, transport: VerificationTransport ): Promise<{ sessionId: string }> { - const { server } = window.textsecure; - strictAssert(server !== undefined, 'WebAPI not available'); - return server.requestVerification(number, captcha, transport); + return doRequestVerification(number, captcha, transport); } function registerSingleDevice( diff --git a/ts/state/smart/CallManager.tsx b/ts/state/smart/CallManager.tsx index 6941075a7b..5f2d019bd8 100644 --- a/ts/state/smart/CallManager.tsx +++ b/ts/state/smart/CallManager.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import lodash from 'lodash'; -import React, { memo } from 'react'; +import React, { memo, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import type { DirectIncomingCall, @@ -51,6 +51,7 @@ import { isSharingPhoneNumberWithEverybody as getIsSharingPhoneNumberWithEverybo import { useGlobalModalActions } from '../ducks/globalModals.js'; import { isLonelyGroup } from '../ducks/callingHelpers.js'; import { getActiveProfile } from '../selectors/notificationProfiles.js'; +import { isOnline as isWebAPIOnline } from '../../textsecure/WebAPI.js'; const { memoize } = lodash; @@ -390,6 +391,24 @@ export const SmartCallManager = memo(function SmartCallManager() { const me = useSelector(getMe); const activeNotificationProfile = useSelector(getActiveProfile); + const [isOnline, setIsOnline] = useState(isWebAPIOnline() ?? false); + + useEffect(() => { + const update = () => { + setIsOnline(isWebAPIOnline() ?? false); + }; + + update(); + + window.Whisper.events.on('online', update); + window.Whisper.events.on('offline', update); + + return () => { + window.Whisper.events.off('online', update); + window.Whisper.events.off('offline', update); + }; + }, []); + const { approveUser, batchUserAction, @@ -457,6 +476,7 @@ export const SmartCallManager = memo(function SmartCallManager() { hangUpActiveCall={hangUpActiveCall} hasInitialLoadCompleted={hasInitialLoadCompleted} i18n={i18n} + isOnline={isOnline} me={me} notifyForCall={notifyForCall} openSystemPreferencesAction={openSystemPreferencesAction} diff --git a/ts/state/smart/CompositionArea.tsx b/ts/state/smart/CompositionArea.tsx index e931c1b686..2fac26e07f 100644 --- a/ts/state/smart/CompositionArea.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -12,6 +12,7 @@ import type { import { hydrateRanges } from '../../types/BodyRange.js'; import { strictAssert } from '../../util/assert.js'; import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation.js'; +import { AutoSubstituteAsciiEmojis } from '../../quill/auto-substitute-ascii-emojis/index.js'; import { imageToBlurHash } from '../../util/imageToBlurHash.js'; import { isConversationSMSOnly } from '../../util/isConversationSMSOnly.js'; import { isSignalConversation } from '../../util/isSignalConversation.js'; @@ -55,6 +56,7 @@ import { isShowingAnyModal } from '../selectors/globalModals.js'; import { isConversationEverUnregistered } from '../../util/isConversationUnregistered.js'; import { isDirectConversation } from '../../util/whatTypeOfConversation.js'; import { isConversationMuted } from '../../util/isConversationMuted.js'; +import { itemStorage } from '../../textsecure/Storage.js'; function renderSmartCompositionRecording() { return ; @@ -219,6 +221,8 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ const { showToast } = useToastActions(); const { onEditorStateChange } = useComposerActions(); + AutoSubstituteAsciiEmojis.enable(itemStorage.get('autoConvertEmoji', true)); + return ( ); } diff --git a/ts/state/smart/LeftPane.tsx b/ts/state/smart/LeftPane.tsx index a9b740130f..863c7bfcd3 100644 --- a/ts/state/smart/LeftPane.tsx +++ b/ts/state/smart/LeftPane.tsx @@ -20,6 +20,9 @@ import { lookupConversationWithoutServiceId } from '../../util/lookupConversatio import { missingCaseError } from '../../util/missingCaseError.js'; import { isDone as isRegistrationDone } from '../../util/registration.js'; import { drop } from '../../util/drop.js'; +import type { ServerAlertsType } from '../../types/ServerAlert.js'; +import { getServerAlertToShow } from '../../util/handleServerAlerts.js'; +import { itemStorage } from '../../textsecure/Storage.js'; import { useCallingActions } from '../ducks/calling.js'; import { useConversationsActions } from '../ducks/conversations.js'; import { @@ -293,6 +296,10 @@ function preloadConversation(conversationId: string): void { ); } +async function saveAlerts(alerts: ServerAlertsType): Promise { + await itemStorage.put('serverAlerts', alerts); +} + export const SmartLeftPane = memo(function SmartLeftPane({ hasFailedStorySends, hasPendingUpdate, @@ -417,6 +424,7 @@ export const SmartLeftPane = memo(function SmartLeftPane({ endConversationSearch={endConversationSearch} endSearch={endSearch} getPreferredBadge={getPreferredBadge} + getServerAlertToShow={getServerAlertToShow} hasExpiredDialog={hasExpiredDialog} hasFailedStorySends={hasFailedStorySends} hasNetworkDialog={hasNetworkDialog} @@ -454,6 +462,7 @@ export const SmartLeftPane = memo(function SmartLeftPane({ renderUnsupportedOSDialog={renderUnsupportedOSDialog} renderUpdateDialog={renderUpdateDialog} resumeBackupMediaDownload={resumeBackupMediaDownload} + saveAlerts={saveAlerts} savePreferredLeftPaneWidth={savePreferredLeftPaneWidth} searchInConversation={searchInConversation} selectedConversationId={selectedConversationId} diff --git a/ts/state/smart/Preferences.tsx b/ts/state/smart/Preferences.tsx index b112a6017b..0fafd24b59 100644 --- a/ts/state/smart/Preferences.tsx +++ b/ts/state/smart/Preferences.tsx @@ -19,7 +19,14 @@ import { getNavTabsCollapsed, getPreferredLeftPaneWidth, } from '../selectors/items.js'; -import { DEFAULT_AUTO_DOWNLOAD_ATTACHMENT } from '../../textsecure/Storage.js'; +import { + itemStorage, + DEFAULT_AUTO_DOWNLOAD_ATTACHMENT, +} from '../../textsecure/Storage.js'; +import { + onHasStoriesDisabledChange, + setPhoneNumberDiscoverability, +} from '../../textsecure/WebAPI.js'; import { DEFAULT_CONVERSATION_COLOR } from '../../types/Colors.js'; import { isBackupFeatureEnabled } from '../../util/isBackupEnabled.js'; import { format } from '../../types/PhoneNumber.js'; @@ -42,7 +49,7 @@ import { } from '../../types/SystemTraySetting.js'; import { calling } from '../../services/calling.js'; import { drop } from '../../util/drop.js'; -import { assertDev, strictAssert } from '../../util/assert.js'; +import { assertDev } from '../../util/assert.js'; import { backupsService } from '../../services/backups/index.js'; import { DurationInSeconds } from '../../util/durations/duration-in-seconds.js'; import { PhoneNumberDiscoverability } from '../../util/phoneNumberDiscoverability.js'; @@ -302,12 +309,12 @@ export function SmartPreferences(): JSX.Element | null { const isSyncSupported = !isPrimary; const [deviceName, setDeviceName] = React.useState( - window.textsecure.storage.user.getDeviceName() + itemStorage.user.getDeviceName() ); useEffect(() => { let canceled = false; const onDeviceNameChanged = () => { - const value = window.textsecure.storage.user.getDeviceName(); + const value = itemStorage.user.getDeviceName(); if (canceled) { return; } @@ -653,7 +660,7 @@ export function SmartPreferences(): JSX.Element | null { async value => { const account = window.ConversationController.getOurConversationOrThrow(); account.captureChange('hasStoriesDisabled'); - window.textsecure.server?.onHasStoriesDisabledChange(value); + onHasStoriesDisabledChange(value); if (!value) { await deleteAllMyStories(); } @@ -690,8 +697,7 @@ export function SmartPreferences(): JSX.Element | null { 'phoneNumberDiscoverability', PhoneNumberDiscoverability.NotDiscoverable, async (newValue: PhoneNumberDiscoverability) => { - strictAssert(window.textsecure.server, 'WebAPI must be available'); - await window.textsecure.server.setPhoneNumberDiscoverability( + await setPhoneNumberDiscoverability( newValue === PhoneNumberDiscoverability.Discoverable ); const account = window.ConversationController.getOurConversationOrThrow(); @@ -740,7 +746,7 @@ export function SmartPreferences(): JSX.Element | null { }); }; - const accountEntropyPool = window.storage.get('accountEntropyPool'); + const accountEntropyPool = itemStorage.get('accountEntropyPool'); return ( diff --git a/ts/state/smart/StoriesSettingsModal.tsx b/ts/state/smart/StoriesSettingsModal.tsx index c9e99c428d..4771f0508f 100644 --- a/ts/state/smart/StoriesSettingsModal.tsx +++ b/ts/state/smart/StoriesSettingsModal.tsx @@ -5,12 +5,12 @@ import React, { memo } from 'react'; import { useSelector } from 'react-redux'; import { StoriesSettingsModal } from '../../components/StoriesSettingsModal.js'; import { - getAllSignalConnections, getCandidateContactsForNewGroup, getConversationByServiceIdSelector, getGroupStories, getMe, } from '../selectors/conversations.js'; +import { getAllSignalConnections } from '../selectors/conversations-extra.js'; import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists.js'; import { getIntl, getTheme } from '../selectors/user.js'; import { getPreferredBadgeSelector } from '../selectors/badges.js'; diff --git a/ts/state/smart/StoryCreator.tsx b/ts/state/smart/StoryCreator.tsx index 6c40090913..d297c050c2 100644 --- a/ts/state/smart/StoryCreator.tsx +++ b/ts/state/smart/StoryCreator.tsx @@ -7,13 +7,14 @@ import { ThemeType } from '../../types/Util.js'; import { LinkPreviewSourceType } from '../../types/LinkPreview.js'; import { StoryCreator } from '../../components/StoryCreator.js'; import { - getAllSignalConnections, getCandidateContactsForNewGroup, + getConversationSelector, getGroupStories, getMe, getNonGroupStories, selectMostRecentActiveStoryTimestampByGroupOrDistributionList, } from '../selectors/conversations.js'; +import { getAllSignalConnections } from '../selectors/conversations-extra.js'; import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists.js'; import { getIntl, @@ -64,6 +65,7 @@ export const SmartStoryCreator = memo(function SmartStoryCreator() { } = useStoryDistributionListsActions(); const { toggleSignalConnectionsModal } = useGlobalModalActions(); + const conversationSelector = useSelector(getConversationSelector); const ourConversationId = useSelector(getUserConversationId); const candidateConversations = useSelector(getCandidateContactsForNewGroup); const distributionLists = useSelector(getDistributionListsWithMembers); @@ -104,6 +106,7 @@ export const SmartStoryCreator = memo(function SmartStoryCreator() { return ( { - drop(window.storage.put('hasCompletedUsernameOnboarding', true)); + drop(itemStorage.put('hasCompletedUsernameOnboarding', true)); }, }; } diff --git a/ts/state/smart/UsernameOnboardingModal.tsx b/ts/state/smart/UsernameOnboardingModal.tsx index 4d7d2ca3ba..7f1c027473 100644 --- a/ts/state/smart/UsernameOnboardingModal.tsx +++ b/ts/state/smart/UsernameOnboardingModal.tsx @@ -10,6 +10,7 @@ import { useGlobalModalActions } from '../ducks/globalModals.js'; import { useUsernameActions } from '../ducks/username.js'; import { useNavActions } from '../ducks/nav.js'; import { NavTab, SettingsPage, ProfileEditorPage } from '../../types/Nav.js'; +import { itemStorage } from '../../textsecure/Storage.js'; export const SmartUsernameOnboardingModal = memo( function SmartUsernameOnboardingModal(): JSX.Element { @@ -19,7 +20,7 @@ export const SmartUsernameOnboardingModal = memo( const { changeLocation } = useNavActions(); const onNext = useCallback(async () => { - await window.storage.put('hasCompletedUsernameOnboarding', true); + await itemStorage.put('hasCompletedUsernameOnboarding', true); openUsernameReservationModal(); changeLocation({ tab: NavTab.Settings, @@ -36,7 +37,7 @@ export const SmartUsernameOnboardingModal = memo( ]); const onSkip = useCallback(async () => { - await window.storage.put('hasCompletedUsernameOnboarding', true); + await itemStorage.put('hasCompletedUsernameOnboarding', true); toggleUsernameOnboarding(); }, [toggleUsernameOnboarding]); diff --git a/ts/test-electron/MessageReceipts_test.ts b/ts/test-electron/MessageReceipts_test.ts index c7434a0c92..153199162b 100644 --- a/ts/test-electron/MessageReceipts_test.ts +++ b/ts/test-electron/MessageReceipts_test.ts @@ -17,20 +17,21 @@ import { messageReceiptTypeSchema, } from '../messageModifiers/MessageReceipts.js'; import { ReadStatus } from '../messages/MessageReadStatus.js'; +import { itemStorage } from '../textsecure/Storage.js'; describe('MessageReceipts', () => { let ourAci: AciString; beforeEach(async () => { ourAci = generateAci(); - await window.textsecure.storage.put('uuid_id', `${ourAci}.1`); - await window.textsecure.storage.put('read-receipt-setting', true); + await itemStorage.put('uuid_id', `${ourAci}.1`); + await itemStorage.put('read-receipt-setting', true); await window.ConversationController.load(); }); afterEach(async () => { await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); }); function generateReceipt( diff --git a/ts/test-electron/MessageReceiver_test.ts b/ts/test-electron/MessageReceiver_test.ts index 20612d2d36..000fc486ce 100644 --- a/ts/test-electron/MessageReceiver_test.ts +++ b/ts/test-electron/MessageReceiver_test.ts @@ -16,6 +16,7 @@ import { SignalService as Proto } from '../protobuf/index.js'; import * as Crypto from '../Crypto.js'; import { toBase64 } from '../Bytes.js'; import { signalProtocolStore } from '../SignalProtocolStore.js'; +import { itemStorage } from '../textsecure/Storage.js'; describe('MessageReceiver', () => { const someAci = generateAci(); @@ -25,15 +26,15 @@ describe('MessageReceiver', () => { let oldDeviceId: number | undefined; beforeEach(async () => { - oldAci = window.storage.user.getAci(); - oldDeviceId = window.storage.user.getDeviceId(); - await window.storage.user.setAciAndDeviceId(generateAci(), 2); + oldAci = itemStorage.user.getAci(); + oldDeviceId = itemStorage.user.getDeviceId(); + await itemStorage.user.setAciAndDeviceId(generateAci(), 2); await signalProtocolStore.hydrateCaches(); }); afterEach(async () => { if (oldAci !== undefined && oldDeviceId !== undefined) { - await window.storage.user.setAciAndDeviceId(oldAci, oldDeviceId); + await itemStorage.user.setAciAndDeviceId(oldAci, oldDeviceId); } await signalProtocolStore.removeAllUnprocessed(); }); @@ -44,7 +45,7 @@ describe('MessageReceiver', () => { fakeTrustRootPublicKey.set([5], 0); // first byte is the key type (5) const messageReceiver = new MessageReceiver({ - storage: window.storage, + storage: itemStorage, serverTrustRoots: [toBase64(fakeTrustRootPublicKey)], }); diff --git a/ts/test-electron/SignalProtocolStore_test.ts b/ts/test-electron/SignalProtocolStore_test.ts index 128c31fd34..154812dcc4 100644 --- a/ts/test-electron/SignalProtocolStore_test.ts +++ b/ts/test-electron/SignalProtocolStore_test.ts @@ -35,6 +35,7 @@ import { Address } from '../types/Address.js'; import { QualifiedAddress } from '../types/QualifiedAddress.js'; import { generateAci, generatePni } from '../types/ServiceId.js'; import type { IdentityKeyType, KeyPairType } from '../textsecure/Types.d.ts'; +import { itemStorage } from '../textsecure/Storage.js'; const { clone } = lodash; @@ -157,16 +158,16 @@ describe('SignalProtocolStore', () => { identityKey = IdentityKeyPair.generate(); testKey = IdentityKeyPair.generate(); - await window.storage.put('registrationIdMap', { + await itemStorage.put('registrationIdMap', { [ourAci]: 1337, }); - await window.storage.put('identityKeyMap', { + await itemStorage.put('identityKeyMap', { [ourAci]: { privKey: identityKey.privateKey.serialize(), pubKey: identityKey.publicKey.serialize(), }, }); - await window.storage.fetch(); + await itemStorage.fetch(); window.ConversationController.reset(); await window.ConversationController.load(); @@ -175,7 +176,7 @@ describe('SignalProtocolStore', () => { after(async () => { await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); }); describe('getLocalRegistrationId', () => { diff --git a/ts/test-electron/background_test.ts b/ts/test-electron/background_test.ts index 53ead4aaab..46689f89f3 100644 --- a/ts/test-electron/background_test.ts +++ b/ts/test-electron/background_test.ts @@ -6,6 +6,7 @@ import lodash from 'lodash'; import { isOverHourIntoPast, cleanupSessionResets } from '../background.js'; import { DataWriter } from '../sql/Client.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { pick } = lodash; @@ -26,13 +27,13 @@ describe('#isOverHourIntoPast', () => { describe('#cleanupSessionResets', () => { after(async () => { await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); }); it('leaves empty object alone', async () => { - await window.storage.put('sessionResets', {}); + await itemStorage.put('sessionResets', {}); await cleanupSessionResets(); - const actual = window.storage.get('sessionResets'); + const actual = itemStorage.get('sessionResets'); const expected = {}; assert.deepEqual(actual, expected); @@ -43,9 +44,9 @@ describe('#cleanupSessionResets', () => { two: Date.now(), three: Date.now() - 65 * 60 * 1000, }; - await window.storage.put('sessionResets', startValue); + await itemStorage.put('sessionResets', startValue); await cleanupSessionResets(); - const actual = window.storage.get('sessionResets'); + const actual = itemStorage.get('sessionResets'); const expected = pick(startValue, ['one', 'two']); assert.deepEqual(actual, expected); @@ -55,9 +56,9 @@ describe('#cleanupSessionResets', () => { one: 0, two: Date.now(), }; - await window.storage.put('sessionResets', startValue); + await itemStorage.put('sessionResets', startValue); await cleanupSessionResets(); - const actual = window.storage.get('sessionResets'); + const actual = itemStorage.get('sessionResets'); const expected = pick(startValue, ['two']); assert.deepEqual(actual, expected); diff --git a/ts/test-electron/backup/attachments_test.ts b/ts/test-electron/backup/attachments_test.ts index 2b3508474f..81e28869eb 100644 --- a/ts/test-electron/backup/attachments_test.ts +++ b/ts/test-electron/backup/attachments_test.ts @@ -42,6 +42,7 @@ import { getPlaintextHashForInMemoryAttachment, } from '../../AttachmentCrypto.js'; import { KIBIBYTE } from '../../types/AttachmentSize.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { omit } = lodash; @@ -55,7 +56,7 @@ describe('backup/attachments', () => { beforeEach(async () => { await DataWriter.removeAll(); - window.storage.reset(); + itemStorage.reset(); window.ConversationController.reset(); diff --git a/ts/test-electron/backup/backup_groupv2_notifications_test.ts b/ts/test-electron/backup/backup_groupv2_notifications_test.ts index 13bf295e10..b8936a9b03 100644 --- a/ts/test-electron/backup/backup_groupv2_notifications_test.ts +++ b/ts/test-electron/backup/backup_groupv2_notifications_test.ts @@ -23,6 +23,7 @@ import { import { ReadStatus } from '../../messages/MessageReadStatus.js'; import { SeenStatus } from '../../MessageSeenStatus.js'; import { loadAllAndReinitializeRedux } from '../../services/allLoaders.js'; +import { itemStorage } from '../../textsecure/Storage.js'; // Note: this should be kept up to date with GroupV2Change.stories.tsx, to // maintain the comprehensive set of GroupV2 notifications we need to handle @@ -77,7 +78,7 @@ describe('backup/groupv2/notifications', () => { beforeEach(async () => { await DataWriter.removeAll(); window.ConversationController.reset(); - window.storage.reset(); + itemStorage.reset(); await setupBasics(); diff --git a/ts/test-electron/backup/bubble_test.ts b/ts/test-electron/backup/bubble_test.ts index 73c44b3b77..7b51a5e741 100644 --- a/ts/test-electron/backup/bubble_test.ts +++ b/ts/test-electron/backup/bubble_test.ts @@ -27,6 +27,7 @@ import type { MessageAttributesType } from '../../model-types.js'; import { IMAGE_PNG, TEXT_ATTACHMENT } from '../../types/MIME.js'; import { MY_STORY_ID } from '../../types/Stories.js'; import { generateAttachmentKeys } from '../../AttachmentCrypto.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const CONTACT_A = generateAci(); const CONTACT_B = generateAci(); @@ -50,7 +51,7 @@ describe('backup/bubble messages', () => { beforeEach(async () => { await DataWriter._removeAllMessages(); await DataWriter._removeAllConversations(); - window.storage.reset(); + itemStorage.reset(); await setupBasics(); diff --git a/ts/test-electron/backup/calling_test.ts b/ts/test-electron/backup/calling_test.ts index 1fde0a38b1..6d8744ac15 100644 --- a/ts/test-electron/backup/calling_test.ts +++ b/ts/test-electron/backup/calling_test.ts @@ -30,6 +30,7 @@ import { ReadStatus } from '../../messages/MessageReadStatus.js'; import { SeenStatus } from '../../MessageSeenStatus.js'; import { deriveGroupID, deriveGroupSecretParams } from '../../util/zkgroup.js'; import { loadAllAndReinitializeRedux } from '../../services/allLoaders.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const CONTACT_A = generateAci(); const GROUP_MASTER_KEY = getRandomBytes(32); @@ -43,7 +44,7 @@ describe('backup/calling', () => { beforeEach(async () => { await DataWriter.removeAll(); window.ConversationController.reset(); - window.storage.reset(); + itemStorage.reset(); await setupBasics(); diff --git a/ts/test-electron/backup/conversations_test.ts b/ts/test-electron/backup/conversations_test.ts index 543ef74499..9e5e23e72b 100644 --- a/ts/test-electron/backup/conversations_test.ts +++ b/ts/test-electron/backup/conversations_test.ts @@ -12,6 +12,7 @@ import { DataWriter } from '../../sql/Client.js'; import { generateAci, generatePni } from '../../types/ServiceId.js'; import type { ConversationAttributesType } from '../../model-types.js'; import { strictAssert } from '../../util/assert.js'; +import { itemStorage } from '../../textsecure/Storage.js'; function getGroupTestInfo() { const masterKey = getRandomBytes(32); @@ -25,7 +26,7 @@ describe('backup/conversations', () => { beforeEach(async () => { await DataWriter._removeAllMessages(); await DataWriter._removeAllConversations(); - window.storage.reset(); + itemStorage.reset(); await setupBasics(); @@ -101,7 +102,7 @@ describe('backup/conversations', () => { } ); - await window.storage.blocked.addBlockedGroup(blockedGroupInfo.groupId); + await itemStorage.blocked.addBlockedGroup(blockedGroupInfo.groupId); await symmetricRoundtripHarness([]); diff --git a/ts/test-electron/backup/filePointer_test.ts b/ts/test-electron/backup/filePointer_test.ts index a451e7b0a5..7bae0ca6d8 100644 --- a/ts/test-electron/backup/filePointer_test.ts +++ b/ts/test-electron/backup/filePointer_test.ts @@ -21,6 +21,7 @@ import { generateKeys } from '../../AttachmentCrypto.js'; import type { GetBackupCdnInfoType } from '../../services/backups/util/mediaId.js'; import { strictAssert } from '../../util/assert.js'; import { isValidAttachmentKey } from '../../types/Crypto.js'; +import { itemStorage } from '../../textsecure/Storage.js'; describe('convertFilePointerToAttachment', () => { const commonFilePointerProps = { @@ -208,7 +209,7 @@ describe('getFilePointerForAttachment', () => { beforeEach(() => { sandbox = sinon.createSandbox(); - sandbox.stub(window.storage, 'get').callsFake(key => { + sandbox.stub(itemStorage, 'get').callsFake(key => { if (key === 'masterKey') { return MASTER_KEY; } diff --git a/ts/test-electron/backup/helpers.ts b/ts/test-electron/backup/helpers.ts index 8a877a0c26..d86153323c 100644 --- a/ts/test-electron/backup/helpers.ts +++ b/ts/test-electron/backup/helpers.ts @@ -28,6 +28,7 @@ import { DataReader, DataWriter } from '../../sql/Client.js'; import { getRandomBytes } from '../../Crypto.js'; import * as Bytes from '../../Bytes.js'; import { postSaveUpdates } from '../../util/cleanup.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { omit, sortBy } = lodash; @@ -258,19 +259,19 @@ export async function asymmetricRoundtripHarness( export async function clearData(): Promise { await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); window.ConversationController.reset(); await setupBasics(); } export async function setupBasics(): Promise { - await window.storage.put('uuid_id', `${OUR_ACI}.2`); - await window.storage.put('pni', OUR_PNI); - await window.storage.put('masterKey', MASTER_KEY); - await window.storage.put('accountEntropyPool', ACCOUNT_ENTROPY_POOL); - await window.storage.put('backupMediaRootKey', MEDIA_ROOT_KEY); - await window.storage.put('profileKey', PROFILE_KEY); + await itemStorage.put('uuid_id', `${OUR_ACI}.2`); + await itemStorage.put('pni', OUR_PNI); + await itemStorage.put('masterKey', MASTER_KEY); + await itemStorage.put('accountEntropyPool', ACCOUNT_ENTROPY_POOL); + await itemStorage.put('backupMediaRootKey', MEDIA_ROOT_KEY); + await itemStorage.put('profileKey', PROFILE_KEY); await window.ConversationController.getOrCreateAndWait(OUR_ACI, 'private', { pni: OUR_PNI, diff --git a/ts/test-electron/backup/non_bubble_test.ts b/ts/test-electron/backup/non_bubble_test.ts index 101a40d6cf..05bdc178af 100644 --- a/ts/test-electron/backup/non_bubble_test.ts +++ b/ts/test-electron/backup/non_bubble_test.ts @@ -25,6 +25,7 @@ import { OUR_ACI, } from './helpers.js'; import { loadAllAndReinitializeRedux } from '../../services/allLoaders.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const CONTACT_A = generateAci(); const GROUP_ID = Bytes.toBase64(getRandomBytes(32)); @@ -36,7 +37,7 @@ describe('backup/non-bubble messages', () => { beforeEach(async () => { await DataWriter._removeAllMessages(); await DataWriter._removeAllConversations(); - window.storage.reset(); + itemStorage.reset(); await setupBasics(); diff --git a/ts/test-electron/models/conversations_test.ts b/ts/test-electron/models/conversations_test.ts index 3f3f62525d..5e0da3c0bd 100644 --- a/ts/test-electron/models/conversations_test.ts +++ b/ts/test-electron/models/conversations_test.ts @@ -11,6 +11,7 @@ import { generateAci, generatePni } from '../../types/ServiceId.js'; import { MessageModel } from '../../models/messages.js'; import { DurationInSeconds } from '../../util/durations/index.js'; import { ConversationModel } from '../../models/conversations.js'; +import { itemStorage } from '../../textsecure/Storage.js'; describe('Conversations', () => { async function resetConversationController(): Promise { @@ -20,12 +21,12 @@ describe('Conversations', () => { after(async () => { await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); }); beforeEach(async () => { await DataWriter.removeAll(); - await window.textsecure.storage.user.setCredentials({ + await itemStorage.user.setCredentials({ number: '+15550000000', aci: generateAci(), pni: generatePni(), diff --git a/ts/test-electron/models/messages_test.ts b/ts/test-electron/models/messages_test.ts index d35fbca46f..0c8463c621 100644 --- a/ts/test-electron/models/messages_test.ts +++ b/ts/test-electron/models/messages_test.ts @@ -11,14 +11,12 @@ import type { ConversationModel } from '../../models/conversations.js'; import type { MessageAttributesType } from '../../model-types.d.ts'; import { MessageModel } from '../../models/messages.js'; import type { RawBodyRange } from '../../types/BodyRange.js'; -import type { WebAPIType } from '../../textsecure/WebAPI.js'; import { DataWriter } from '../../sql/Client.js'; -import MessageSender from '../../textsecure/SendMessage.js'; import enMessages from '../../../_locales/en/messages.json'; import { SendStatus } from '../../messages/MessageSendState.js'; import { SignalService as Proto } from '../../protobuf/index.js'; import { generateAci } from '../../types/ServiceId.js'; -import { getAuthor } from '../../messages/helpers.js'; +import { getAuthor } from '../../messages/sources.js'; import { setupI18n } from '../../util/setupI18n.js'; import { APPLICATION_JSON, @@ -32,6 +30,8 @@ import { import { getNotificationDataForMessage } from '../../util/getNotificationDataForMessage.js'; import { getNotificationTextForMessage } from '../../util/getNotificationTextForMessage.js'; import { send } from '../../messages/send.js'; +import { messageSender } from '../../textsecure/SendMessage.js'; +import { itemStorage } from '../../textsecure/Storage.js'; describe('Message', () => { const i18n = setupI18n('en', enMessages); @@ -72,13 +72,13 @@ describe('Message', () => { window.ConversationController.reset(); await window.ConversationController.load(); - await window.textsecure.storage.put('number_id', `${me}.2`); - await window.textsecure.storage.put('uuid_id', `${ourServiceId}.2`); + await itemStorage.put('number_id', `${me}.2`); + await itemStorage.put('uuid_id', `${ourServiceId}.2`); }); after(async () => { await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); }); beforeEach(function (this: Mocha.Context) { @@ -91,27 +91,8 @@ describe('Message', () => { // NOTE: These tests are incomplete. describe('send', () => { - let oldMessageSender: undefined | MessageSender; - beforeEach(function (this: Mocha.Context) { - oldMessageSender = window.textsecure.messaging; - - window.textsecure.messaging = - oldMessageSender ?? new MessageSender({} as WebAPIType); - this.sandbox - .stub(window.textsecure.messaging, 'sendSyncMessage') - .resolves({}); - }); - - afterEach(() => { - if (oldMessageSender) { - window.textsecure.messaging = oldMessageSender; - } else { - // `window.textsecure.messaging` can be undefined in tests. Instead of updating - // the real type, I just ignore it. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (window.textsecure as any).messaging; - } + this.sandbox.stub(messageSender, 'sendSyncMessage').resolves({}); }); it('updates `sendStateByConversationId`', async function (this: Mocha.Context) { diff --git a/ts/test-electron/services/AttachmentBackupManager_test.ts b/ts/test-electron/services/AttachmentBackupManager_test.ts index b571ec6e9c..167f53d8ad 100644 --- a/ts/test-electron/services/AttachmentBackupManager_test.ts +++ b/ts/test-electron/services/AttachmentBackupManager_test.ts @@ -26,6 +26,7 @@ import { createName, getRelativePath } from '../../util/attachmentPath.js'; import { encryptAttachmentV2, generateKeys } from '../../AttachmentCrypto.js'; import { SECOND } from '../../util/durations/index.js'; import { HTTPError } from '../../types/HTTPError.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { ensureFile } = fsExtra; @@ -115,8 +116,8 @@ describe('AttachmentBackupManager/JobManager', function attachmentBackupManager( beforeEach(async () => { await DataWriter.removeAll(); - await window.storage.put('masterKey', Bytes.toBase64(getRandomBytes(32))); - await window.storage.put('backupMediaRootKey', getRandomBytes(32)); + await itemStorage.put('masterKey', Bytes.toBase64(getRandomBytes(32))); + await itemStorage.put('backupMediaRootKey', getRandomBytes(32)); sandbox = sinon.createSandbox(); clock = sandbox.useFakeTimers(); @@ -169,7 +170,7 @@ describe('AttachmentBackupManager/JobManager', function attachmentBackupManager( sandbox.restore(); await backupManager?.stop(); await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); }); async function addJobs( diff --git a/ts/test-electron/services/AttachmentDownloadManager_test.ts b/ts/test-electron/services/AttachmentDownloadManager_test.ts index 3a4a50cff3..fec7f5ae16 100644 --- a/ts/test-electron/services/AttachmentDownloadManager_test.ts +++ b/ts/test-electron/services/AttachmentDownloadManager_test.ts @@ -40,6 +40,7 @@ import { explodePromise, type ExplodePromiseResultType, } from '../../util/explodePromise.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { omit } = lodash; @@ -107,7 +108,7 @@ describe('AttachmentDownloadManager', () => { beforeEach(async () => { await DataWriter.removeAll(); - await window.storage.user.setAciAndDeviceId(generateAci(), 1); + await itemStorage.user.setAciAndDeviceId(generateAci(), 1); sandbox = sinon.createSandbox(); clock = sandbox.useFakeTimers(); @@ -117,7 +118,7 @@ describe('AttachmentDownloadManager', () => { onLowDiskSpaceBackupImport = sandbox .stub() .callsFake(async () => - window.storage.put('backupMediaDownloadPaused', true) + itemStorage.put('backupMediaDownloadPaused', true) ); runJob = sandbox .stub< @@ -162,7 +163,7 @@ describe('AttachmentDownloadManager', () => { await downloadManager?.stop(); sandbox.restore(); await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); }); async function addJob( @@ -375,12 +376,12 @@ describe('AttachmentDownloadManager', () => { assert.strictEqual(runJob.callCount, 0); assert.strictEqual(onLowDiskSpaceBackupImport.callCount, 1); - assert.isTrue(window.storage.get('backupMediaDownloadPaused')); + assert.isTrue(itemStorage.get('backupMediaDownloadPaused')); statfs.callsFake(() => Promise.resolve({ bavail: 100_000_000_000, bsize: 8 }) ); - await window.storage.put('backupMediaDownloadPaused', false); + await itemStorage.put('backupMediaDownloadPaused', false); await advanceTime(2 * MINUTE); assert.strictEqual(runJob.callCount, 1); @@ -501,7 +502,7 @@ describe('AttachmentDownloadManager', () => { }); it('only selects backup_import jobs if the mediaDownload is not paused', async () => { - await window.storage.put('backupMediaDownloadPaused', true); + await itemStorage.put('backupMediaDownloadPaused', true); const jobs = await addJobs(6, idx => ({ source: @@ -518,7 +519,7 @@ describe('AttachmentDownloadManager', () => { assertRunJobCalledWith([jobs[1], jobs[5], jobs[3]]); // resume backups - await window.storage.put('backupMediaDownloadPaused', false); + await itemStorage.put('backupMediaDownloadPaused', false); await advanceTime((downloadManager?.tickInterval ?? MINUTE) * 5); assertRunJobCalledWith([ jobs[1], diff --git a/ts/test-electron/services/MessageCache_test.ts b/ts/test-electron/services/MessageCache_test.ts index 454298af5f..4476646fd2 100644 --- a/ts/test-electron/services/MessageCache_test.ts +++ b/ts/test-electron/services/MessageCache_test.ts @@ -9,17 +9,18 @@ import { strictAssert } from '../../util/assert.js'; import { MessageCache } from '../../services/MessageCache.js'; import { generateAci } from '../../types/ServiceId.js'; import { DataWriter } from '../../sql/Client.js'; +import { itemStorage } from '../../textsecure/Storage.js'; describe('MessageCache', () => { beforeEach(async () => { const ourAci = generateAci(); - await window.textsecure.storage.put('uuid_id', `${ourAci}.1`); + await itemStorage.put('uuid_id', `${ourAci}.1`); await window.ConversationController.load(); }); afterEach(async () => { await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); }); describe('findBySentAt', () => { diff --git a/ts/test-electron/services/ReleaseNotesFetcher_test.ts b/ts/test-electron/services/ReleaseNotesFetcher_test.ts index e74c1b2157..fc7552d66f 100644 --- a/ts/test-electron/services/ReleaseNotesFetcher_test.ts +++ b/ts/test-electron/services/ReleaseNotesFetcher_test.ts @@ -10,9 +10,9 @@ import { ReleaseNotesFetcher } from '../../services/releaseNotesFetcher.js'; import * as durations from '../../util/durations/index.js'; import { generateAci } from '../../types/ServiceId.js'; import { saveNewMessageBatcher } from '../../util/messageBatcher.js'; -import type { WebAPIType } from '../../textsecure/WebAPI.js'; import type { CIType } from '../../CI.js'; import type { ConversationModel } from '../../models/conversations.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const waitUntil = ( condition: () => boolean, @@ -76,7 +76,6 @@ describe('ReleaseNotesFetcher', () => { let sandbox = sinon.createSandbox(); let clock: sinon.SinonFakeTimers | undefined; - let originalTextsecureServer: WebAPIType | undefined; let originalSignalCI: CIType | undefined; async function setupTest(options: TestSetupOptions = {}) { @@ -122,14 +121,8 @@ describe('ReleaseNotesFetcher', () => { sandbox.stub(window.MessageCache, 'register').callsFake(message => message); // Save original values before modifying - originalTextsecureServer = window.textsecure.server; originalSignalCI = window.SignalCI; - // Initialize textsecure.server if needed - if (!window.textsecure.server) { - window.textsecure.server = {} as unknown as WebAPIType; - } - // Stub server methods const serverStubs = { isOnline: sandbox.stub().returns(isOnline), @@ -159,8 +152,6 @@ describe('ReleaseNotesFetcher', () => { }), }; - sandbox.stub(window.textsecure, 'server').value(serverStubs); - // Stub other globals sandbox.stub(window.SignalContext, 'getI18nLocale').returns('en-US'); sandbox.stub(window, 'getVersion').returns(currentVersion); @@ -192,7 +183,7 @@ describe('ReleaseNotesFetcher', () => { // Helper to run fetcher and wait for completion const runFetcherAndWaitForCompletion = async () => { - await ReleaseNotesFetcher.init(events, isNewVersion); + await ReleaseNotesFetcher.init(serverStubs, events, isNewVersion); // Wait for SignalCI.handleEvent to be called const signalCI = window.SignalCI as unknown as { @@ -204,45 +195,40 @@ describe('ReleaseNotesFetcher', () => { // Storage setup helper const setupStorage = async () => { // Set up storage values - await window.storage.put('chromiumRegistrationDone', ''); + await itemStorage.put('chromiumRegistrationDone', ''); if (storedVersionWatermark !== undefined) { - await window.textsecure.storage.put( + await itemStorage.put( VERSION_WATERMARK_STORAGE_KEY, storedVersionWatermark ); } else { - await window.textsecure.storage.remove(VERSION_WATERMARK_STORAGE_KEY); + await itemStorage.remove(VERSION_WATERMARK_STORAGE_KEY); } if (storedPreviousManifestHash !== undefined) { - await window.textsecure.storage.put( + await itemStorage.put( PREVIOUS_MANIFEST_HASH_STORAGE_KEY, storedPreviousManifestHash ); } else { - await window.textsecure.storage.remove( - PREVIOUS_MANIFEST_HASH_STORAGE_KEY - ); + await itemStorage.remove(PREVIOUS_MANIFEST_HASH_STORAGE_KEY); } if (storedNextFetchTime !== undefined) { - await window.textsecure.storage.put( - NEXT_FETCH_TIME_STORAGE_KEY, - storedNextFetchTime - ); + await itemStorage.put(NEXT_FETCH_TIME_STORAGE_KEY, storedNextFetchTime); } else { - await window.textsecure.storage.remove(NEXT_FETCH_TIME_STORAGE_KEY); + await itemStorage.remove(NEXT_FETCH_TIME_STORAGE_KEY); } }; // Helper functions to get current storage values const getCurrentHash = () => { - return window.textsecure.storage.get(PREVIOUS_MANIFEST_HASH_STORAGE_KEY); + return itemStorage.get(PREVIOUS_MANIFEST_HASH_STORAGE_KEY); }; const getCurrentWatermark = () => { - return window.textsecure.storage.get(VERSION_WATERMARK_STORAGE_KEY); + return itemStorage.get(VERSION_WATERMARK_STORAGE_KEY); }; return { @@ -281,11 +267,10 @@ describe('ReleaseNotesFetcher', () => { clock?.restore(); // Restore original global values (even if they were undefined) - window.textsecure.server = originalTextsecureServer as WebAPIType; window.SignalCI = originalSignalCI as CIType; // Reset storage state - await window.storage.fetch(); + await itemStorage.fetch(); // Reset conversation controller for next test window.ConversationController.reset(); diff --git a/ts/test-electron/state/ducks/conversations_test.ts b/ts/test-electron/state/ducks/conversations_test.ts index 77087c8f98..8fe02ad45f 100644 --- a/ts/test-electron/state/ducks/conversations_test.ts +++ b/ts/test-electron/state/ducks/conversations_test.ts @@ -69,6 +69,7 @@ import { import { MY_STORY_ID } from '../../../types/Stories.js'; import type { ReadonlyMessageAttributesType } from '../../../model-types.d.ts'; import { strictAssert } from '../../../util/assert.js'; +import { itemStorage } from '../../../textsecure/Storage.js'; const { times } = lodash; @@ -2260,7 +2261,7 @@ describe('both/state/ducks/conversations', () => { assert.isUndefined( nextState.conversationsByGroupId.jkl.conversationColor ); - await window.storage.remove('defaultConversationColor'); + await itemStorage.remove('defaultConversationColor'); }); }); diff --git a/ts/test-electron/state/ducks/stories_test.ts b/ts/test-electron/state/ducks/stories_test.ts index 300b5db5a9..eefb95b45f 100644 --- a/ts/test-electron/state/ducks/stories_test.ts +++ b/ts/test-electron/state/ducks/stories_test.ts @@ -28,18 +28,19 @@ import { reducer as rootReducer } from '../../../state/reducer.js'; import { dropNull } from '../../../util/dropNull.js'; import { MessageModel } from '../../../models/messages.js'; import { DataWriter } from '../../../sql/Client.js'; +import { itemStorage } from '../../../textsecure/Storage.js'; describe('both/state/ducks/stories', () => { const ourAci = generateAci(); const deviceId = 2; before(async () => { - await window.textsecure.storage.put('uuid_id', `${ourAci}.${deviceId}`); + await itemStorage.put('uuid_id', `${ourAci}.${deviceId}`); }); after(async () => { await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); }); const getEmptyRootState = () => ({ diff --git a/ts/test-electron/textsecure/AccountManager_test.ts b/ts/test-electron/textsecure/AccountManager_test.ts index 4cf18ae179..d3f4a4c5e7 100644 --- a/ts/test-electron/textsecure/AccountManager_test.ts +++ b/ts/test-electron/textsecure/AccountManager_test.ts @@ -20,6 +20,7 @@ import { } from '../../types/ServiceId.js'; import { DAY } from '../../util/durations/index.js'; import { signalProtocolStore } from '../../SignalProtocolStore.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { range } = lodash; @@ -27,7 +28,7 @@ const { range } = lodash; describe('AccountManager', () => { let sandbox: sinon.SinonSandbox; - let accountManager: AccountManager; + const accountManager = new AccountManager(); const ourAci = generateAci(); const ourPni = generatePni(); @@ -38,13 +39,10 @@ describe('AccountManager', () => { beforeEach(() => { sandbox = sinon.createSandbox(); - const server: any = {}; - accountManager = new AccountManager(server); - sandbox .stub(signalProtocolStore, 'getIdentityKeyPair') .returns(identityKey); - const { user } = window.textsecure.storage; + const { user } = itemStorage; sandbox.stub(user, 'getAci').returns(ourAci); sandbox.stub(user, 'getPni').returns(ourPni); sandbox.stub(user, 'getServiceId').returns(ourAci); diff --git a/ts/test-electron/textsecure/KeyChangeListener_test.ts b/ts/test-electron/textsecure/KeyChangeListener_test.ts index 17d2444d92..84147844a8 100644 --- a/ts/test-electron/textsecure/KeyChangeListener_test.ts +++ b/ts/test-electron/textsecure/KeyChangeListener_test.ts @@ -11,6 +11,7 @@ import { explodePromise } from '../../util/explodePromise.js'; import { SignalProtocolStore } from '../../SignalProtocolStore.js'; import type { ConversationModel } from '../../models/conversations.js'; import * as KeyChangeListener from '../../textsecure/KeyChangeListener.js'; +import { itemStorage } from '../../textsecure/Storage.js'; import * as Bytes from '../../Bytes.js'; import { cleanupMessages } from '../../util/cleanup.js'; @@ -29,25 +30,22 @@ describe('KeyChangeListener', () => { window.ConversationController.reset(); await window.ConversationController.load(); - const { storage } = window.textsecure; - - oldNumberId = storage.get('number_id'); - oldUuidId = storage.get('uuid_id'); - await storage.put('number_id', '+14155555556.2'); - await storage.put('uuid_id', `${ourServiceId}.2`); + oldNumberId = itemStorage.get('number_id'); + oldUuidId = itemStorage.get('uuid_id'); + await itemStorage.put('number_id', '+14155555556.2'); + await itemStorage.put('uuid_id', `${ourServiceId}.2`); }); after(async () => { await DataWriter.removeAll(); - const { storage } = window.textsecure; - await storage.fetch(); + await itemStorage.fetch(); if (oldNumberId) { - await storage.put('number_id', oldNumberId); + await itemStorage.put('number_id', oldNumberId); } if (oldUuidId) { - await storage.put('uuid_id', oldUuidId); + await itemStorage.put('uuid_id', oldUuidId); } }); diff --git a/ts/test-electron/textsecure/generate_keys_test.ts b/ts/test-electron/textsecure/generate_keys_test.ts index 7b2c682954..28c00edc9a 100644 --- a/ts/test-electron/textsecure/generate_keys_test.ts +++ b/ts/test-electron/textsecure/generate_keys_test.ts @@ -14,8 +14,7 @@ import { ServiceIdKind } from '../../types/ServiceId.js'; import { normalizeAci } from '../../util/normalizeAci.js'; import { DataWriter } from '../../sql/Client.js'; import { signalProtocolStore } from '../../SignalProtocolStore.js'; - -const { textsecure } = window; +import { itemStorage } from '../../textsecure/Storage.js'; const assertEqualBuffers = (a: Uint8Array, b: Uint8Array) => { assert.isTrue(constantTimeEqual(a, b)); @@ -68,13 +67,13 @@ describe('Key generation', function (this: Mocha.Suite) { await signalProtocolStore.clearSignedPreKeysStore(); const keyPair = generateKeyPair(); - await textsecure.storage.put('identityKeyMap', { + await itemStorage.put('identityKeyMap', { [ourServiceId]: { pubKey: keyPair.publicKey.serialize(), privKey: keyPair.privateKey.serialize(), }, }); - await textsecure.storage.user.setAciAndDeviceId(ourServiceId, 1); + await itemStorage.user.setAciAndDeviceId(ourServiceId, 1); await signalProtocolStore.hydrateCaches(); }); @@ -85,13 +84,12 @@ describe('Key generation', function (this: Mocha.Suite) { await signalProtocolStore.clearSignedPreKeysStore(); await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); }); describe('the first time', () => { before(async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const accountManager = new AccountManager({} as any); + const accountManager = new AccountManager(); result = await accountManager._generateSingleUseKeys( ServiceIdKind.ACI, count @@ -128,8 +126,7 @@ describe('Key generation', function (this: Mocha.Suite) { }); describe('the second time', () => { before(async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const accountManager = new AccountManager({} as any); + const accountManager = new AccountManager(); result = await accountManager._generateSingleUseKeys( ServiceIdKind.ACI, count @@ -166,8 +163,7 @@ describe('Key generation', function (this: Mocha.Suite) { }); describe('the third time, after keys are confirmed', () => { before(async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const accountManager = new AccountManager({} as any); + const accountManager = new AccountManager(); await accountManager._confirmKeys(result, ServiceIdKind.ACI); diff --git a/ts/test-electron/updateConversationsWithUuidLookup_test.ts b/ts/test-electron/updateConversationsWithUuidLookup_test.ts index 211ddf23e3..ee6d35b538 100644 --- a/ts/test-electron/updateConversationsWithUuidLookup_test.ts +++ b/ts/test-electron/updateConversationsWithUuidLookup_test.ts @@ -9,11 +9,13 @@ import sinon from 'sinon'; import { DataWriter } from '../sql/Client.js'; import { ConversationModel } from '../models/conversations.js'; import type { ConversationAttributesType } from '../model-types.d.ts'; -import type { WebAPIType } from '../textsecure/WebAPI.js'; import { generateAci, normalizeServiceId } from '../types/ServiceId.js'; import { normalizeAci } from '../util/normalizeAci.js'; -import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup.js'; +import { + updateConversationsWithUuidLookup, + type ServerType, +} from '../updateConversationsWithUuidLookup.js'; describe('updateConversationsWithUuidLookup', () => { class FakeConversationController { @@ -148,7 +150,7 @@ describe('updateConversationsWithUuidLookup', () => { let fakeCdsLookup: sinon.SinonStub; let fakeCheckAccountExistence: sinon.SinonStub; - let fakeServer: Pick; + let fakeServer: ServerType; beforeEach(() => { sinonSandbox = sinon.createSandbox(); diff --git a/ts/test-electron/util/downloadAttachment_test.ts b/ts/test-electron/util/downloadAttachment_test.ts index 0c830065ba..d2d3b7afe5 100644 --- a/ts/test-electron/util/downloadAttachment_test.ts +++ b/ts/test-electron/util/downloadAttachment_test.ts @@ -10,10 +10,7 @@ import { IMAGE_PNG } from '../../types/MIME.js'; import { downloadAttachment } from '../../util/downloadAttachment.js'; import { MediaTier } from '../../types/AttachmentDownload.js'; import { HTTPError } from '../../types/HTTPError.js'; -import { - getCdnNumberForBackupTier, - type downloadAttachment as downloadAttachmentFromServer, -} from '../../textsecure/downloadAttachment.js'; +import { getCdnNumberForBackupTier } from '../../textsecure/downloadAttachment.js'; import { MASTER_KEY, MEDIA_ROOT_KEY } from '../backup/helpers.js'; import { getMediaIdFromMediaName } from '../../services/backups/util/mediaId.js'; import { @@ -21,10 +18,10 @@ import { AttachmentPermanentlyUndownloadableError, } from '../../types/Attachment.js'; import { updateRemoteConfig } from '../../test-helpers/RemoteConfigStub.js'; -import type { WebAPIType } from '../../textsecure/WebAPI.js'; import { toHex, toBase64 } from '../../Bytes.js'; import { generateAttachmentKeys } from '../../AttachmentCrypto.js'; import { getRandomBytes } from '../../Crypto.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { noop } = lodash; @@ -43,21 +40,11 @@ describe('utils/downloadAttachment', () => { }; const abortController = new AbortController(); - let sandbox: sinon.SinonSandbox; - const fakeServer = {} as WebAPIType; - beforeEach(() => { - sandbox = sinon.createSandbox(); - sandbox.stub(window, 'textsecure').value({ server: fakeServer }); - }); - afterEach(() => { - sandbox.restore(); - }); - function assertDownloadArgs( - actual: unknown, - expected: Parameters + actual: Array, + expected: Array ) { - assert.deepStrictEqual(actual, expected); + assert.deepStrictEqual(actual.slice(1), expected); } it('downloads from transit tier first if no backup information', async () => { @@ -78,7 +65,6 @@ describe('utils/downloadAttachment', () => { }); assert.equal(stubDownload.callCount, 1); assertDownloadArgs(stubDownload.getCall(0).args, [ - fakeServer, { attachment, mediaTier: MediaTier.STANDARD }, { variant: AttachmentVariant.Default, @@ -115,7 +101,6 @@ describe('utils/downloadAttachment', () => { assert.equal(stubDownload.callCount, 1); assertDownloadArgs(stubDownload.getCall(0).args, [ - fakeServer, { attachment, mediaTier: MediaTier.STANDARD }, { variant: AttachmentVariant.Default, @@ -152,7 +137,6 @@ describe('utils/downloadAttachment', () => { assert.equal(stubDownload.callCount, 1); assertDownloadArgs(stubDownload.getCall(0).args, [ - fakeServer, { attachment, mediaTier: MediaTier.STANDARD }, { variant: AttachmentVariant.Default, @@ -207,7 +191,6 @@ describe('utils/downloadAttachment', () => { }); assert.equal(stubDownload.callCount, 1); assertDownloadArgs(stubDownload.getCall(0).args, [ - fakeServer, { attachment, mediaTier: MediaTier.BACKUP }, { variant: AttachmentVariant.Default, @@ -240,7 +223,6 @@ describe('utils/downloadAttachment', () => { }); assert.equal(stubDownload.callCount, 2); assertDownloadArgs(stubDownload.getCall(0).args, [ - fakeServer, { attachment, mediaTier: MediaTier.BACKUP }, { variant: AttachmentVariant.Default, @@ -250,7 +232,6 @@ describe('utils/downloadAttachment', () => { }, ]); assertDownloadArgs(stubDownload.getCall(1).args, [ - fakeServer, { attachment, mediaTier: MediaTier.STANDARD, @@ -286,7 +267,6 @@ describe('utils/downloadAttachment', () => { }); assert.equal(stubDownload.callCount, 2); assertDownloadArgs(stubDownload.getCall(0).args, [ - fakeServer, { attachment, mediaTier: MediaTier.BACKUP }, { variant: AttachmentVariant.Default, @@ -296,7 +276,6 @@ describe('utils/downloadAttachment', () => { }, ]); assertDownloadArgs(stubDownload.getCall(1).args, [ - fakeServer, { attachment, mediaTier: MediaTier.STANDARD }, { variant: AttachmentVariant.Default, @@ -332,7 +311,6 @@ describe('utils/downloadAttachment', () => { ); assert.equal(stubDownload.callCount, 2); assertDownloadArgs(stubDownload.getCall(0).args, [ - fakeServer, { attachment, mediaTier: MediaTier.BACKUP }, { variant: AttachmentVariant.Default, @@ -342,7 +320,6 @@ describe('utils/downloadAttachment', () => { }, ]); assertDownloadArgs(stubDownload.getCall(1).args, [ - fakeServer, { attachment, mediaTier: MediaTier.STANDARD }, { variant: AttachmentVariant.Default, @@ -360,7 +337,7 @@ describe('getCdnNumberForBackupTier', () => { beforeEach(async () => { await DataWriter.removeAll(); sandbox = sinon.createSandbox(); - sandbox.stub(window.storage, 'get').callsFake(key => { + sandbox.stub(itemStorage, 'get').callsFake(key => { if (key === 'masterKey') { return MASTER_KEY; } diff --git a/ts/test-electron/util/migrateMessageData_test.ts b/ts/test-electron/util/migrateMessageData_test.ts index 51e6f21c88..309266cf06 100644 --- a/ts/test-electron/util/migrateMessageData_test.ts +++ b/ts/test-electron/util/migrateMessageData_test.ts @@ -8,6 +8,7 @@ import type { MessageAttributesType } from '../../model-types.js'; import { DataReader, DataWriter } from '../../sql/Client.js'; import { generateAci } from '../../types/ServiceId.js'; import { postSaveUpdates } from '../../util/cleanup.js'; +import { itemStorage } from '../../textsecure/Storage.js'; function composeMessage(timestamp: number): MessageAttributesType { return { @@ -25,11 +26,11 @@ function composeMessage(timestamp: number): MessageAttributesType { describe('utils/migrateMessageData', async () => { before(async () => { await DataWriter.removeAll(); - await window.storage.put('uuid_id', generateAci()); + await itemStorage.put('uuid_id', generateAci()); }); after(async () => { await DataWriter.removeAll(); - await window.storage.fetch(); + await itemStorage.fetch(); }); it('increments attempts for messages which fail to save', async () => { const messages = new Array(5) diff --git a/ts/test-electron/util/reactions_test.ts b/ts/test-electron/util/reactions_test.ts index 81f06ef36a..7770eecc4c 100644 --- a/ts/test-electron/util/reactions_test.ts +++ b/ts/test-electron/util/reactions_test.ts @@ -11,6 +11,7 @@ import { incrementMessageCounter } from '../../util/incrementMessageCounter.js'; import type { ConversationModel } from '../../models/conversations.js'; import type { MessageAttributesType } from '../../model-types.js'; import { SendStatus } from '../../messages/MessageSendState.js'; +import { itemStorage } from '../../textsecure/Storage.js'; describe('isMessageAMatchForReaction', () => { let contactA: ConversationModel; @@ -21,7 +22,7 @@ describe('isMessageAMatchForReaction', () => { const OUR_PNI = generatePni(); beforeEach(async () => { await DataWriter.removeAll(); - await window.textsecure.storage.user.setCredentials({ + await itemStorage.user.setCredentials({ number: '+15550000000', aci: OUR_ACI, pni: OUR_PNI, diff --git a/ts/test-helpers/RemoteConfigStub.ts b/ts/test-helpers/RemoteConfigStub.ts index d1183ae9bc..6d141bc4fe 100644 --- a/ts/test-helpers/RemoteConfigStub.ts +++ b/ts/test-helpers/RemoteConfigStub.ts @@ -2,24 +2,33 @@ // SPDX-License-Identifier: AGPL-3.0-only import { _refreshRemoteConfig } from '../RemoteConfig.js'; -import type { - WebAPIType, - RemoteConfigResponseType, -} from '../textsecure/WebAPI.js'; +import type { RemoteConfigResponseType } from '../textsecure/WebAPI.js'; export async function updateRemoteConfig( newConfig: Array<{ name: string; value: string }> ): Promise { - const fakeServer = { - async getConfig(): Promise { - const serverTimestamp = Date.now(); - return { - config: new Map(newConfig.map(({ name, value }) => [name, value])), - serverTimestamp, - configHash: serverTimestamp.toString(), - }; - }, - } as Partial as unknown as WebAPIType; + async function getConfig(): Promise { + const serverTimestamp = Date.now(); + return { + config: new Map(newConfig.map(({ name, value }) => [name, value])), + serverTimestamp, + configHash: serverTimestamp.toString(), + }; + } - await _refreshRemoteConfig(fakeServer); + const storageMap = new Map(); + const storage = { + get: (key: string): unknown => storageMap.get(key), + put: async (key: string, value: unknown): Promise => { + storageMap.set(key, value); + }, + remove: async (key: string): Promise => { + storageMap.delete(key); + }, + }; + + await _refreshRemoteConfig({ + getConfig, + storage, + }); } diff --git a/ts/test-node/services/retryPlaceholders_test.ts b/ts/test-node/services/retryPlaceholders_test.ts index f1c2cf92d2..64743359cb 100644 --- a/ts/test-node/services/retryPlaceholders_test.ts +++ b/ts/test-node/services/retryPlaceholders_test.ts @@ -12,23 +12,29 @@ import { STORAGE_KEY, } from '../../services/retryPlaceholders.js'; -/* eslint-disable @typescript-eslint/no-explicit-any */ - describe('RetryPlaceholders', () => { const NOW = 1_000_000; - let clock: any; + let sandbox: sinon.SinonSandbox; + + const storageMap = new Map(); + const storage = { + get: (key: string): unknown => storageMap.get(key), + put: async (key: string, value: unknown): Promise => { + storageMap.set(key, value); + }, + }; beforeEach(async () => { - await window.storage.put(STORAGE_KEY, undefined as any); + storageMap.clear(); - clock = sinon.useFakeTimers({ + sandbox = sinon.createSandbox(); + sandbox.useFakeTimers({ now: NOW, }); }); afterEach(async () => { - await window.storage.remove(STORAGE_KEY); - clock.restore(); + sandbox.restore(); }); function getDefaultItem(): RetryItemType { @@ -47,21 +53,21 @@ describe('RetryPlaceholders', () => { getDefaultItem(), { ...getDefaultItem(), conversationId: 'conversation-id-2' }, ]; - await window.storage.put(STORAGE_KEY, items); + await storage.put(STORAGE_KEY, items); const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(2, placeholders.getCount()); }); it('starts with no data if provided data fails to parse', async () => { - await window.storage.put(STORAGE_KEY, [ + await storage.put(STORAGE_KEY, [ { item: 'is wrong shape!' }, { bad: 'is not good!' }, - ] as any); + ]); const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(0, placeholders.getCount()); }); @@ -70,18 +76,18 @@ describe('RetryPlaceholders', () => { describe('#add', () => { it('adds one item', async () => { const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); await placeholders.add(getDefaultItem()); assert.strictEqual(1, placeholders.getCount()); }); it('throws if provided data fails to parse', async () => { const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); await assert.isRejected( placeholders.add({ item: 'is wrong shape!', - } as any), + } as unknown as RetryItemType), 'Item did not match schema' ); }); @@ -90,17 +96,17 @@ describe('RetryPlaceholders', () => { describe('#getNextToExpire', () => { it('returns nothing if no items', () => { const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(0, placeholders.getCount()); assert.isUndefined(placeholders.getNextToExpire()); }); it('returns only item if just one item', async () => { const item = getDefaultItem(); const items: Array = [item]; - await window.storage.put(STORAGE_KEY, items); + await storage.put(STORAGE_KEY, items); const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(1, placeholders.getCount()); assert.deepEqual(item, placeholders.getNextToExpire()); }); @@ -114,10 +120,10 @@ describe('RetryPlaceholders', () => { receivedAt: NOW + 10, }; const items: Array = [older, newer]; - await window.storage.put(STORAGE_KEY, items); + await storage.put(STORAGE_KEY, items); const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(2, placeholders.getCount()); assert.deepEqual(older, placeholders.getNextToExpire()); @@ -143,10 +149,10 @@ describe('RetryPlaceholders', () => { receivedAt: NOW + 15, }; const items: Array = [older, newer]; - await window.storage.put(STORAGE_KEY, items); + await storage.put(STORAGE_KEY, items); const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(2, placeholders.getCount()); assert.deepEqual([], await placeholders.getExpiredAndRemove()); assert.strictEqual(2, placeholders.getCount()); @@ -161,10 +167,10 @@ describe('RetryPlaceholders', () => { receivedAt: NOW + 15, }; const items: Array = [older, newer]; - await window.storage.put(STORAGE_KEY, items); + await storage.put(STORAGE_KEY, items); const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(2, placeholders.getCount()); assert.deepEqual([older], await placeholders.getExpiredAndRemove()); assert.strictEqual(1, placeholders.getCount()); @@ -180,10 +186,10 @@ describe('RetryPlaceholders', () => { receivedAt: getDeltaIntoPast() - 900, }; const items: Array = [older, newer]; - await window.storage.put(STORAGE_KEY, items); + await storage.put(STORAGE_KEY, items); const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(2, placeholders.getCount()); assert.deepEqual( [older, newer], @@ -204,15 +210,15 @@ describe('RetryPlaceholders', () => { conversationId: 'conversation-id-2', }; const items: Array = [older, newer]; - await window.storage.put(STORAGE_KEY, items); + await storage.put(STORAGE_KEY, items); const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(2, placeholders.getCount()); await placeholders.findByConversationAndMarkOpened('conversation-id-3'); assert.strictEqual(2, placeholders.getCount()); - const saveItems = window.storage.get(STORAGE_KEY); + const saveItems = storage.get(STORAGE_KEY); assert.deepEqual([older, newer], saveItems); }); it('updates all items matching conversation', async () => { @@ -232,15 +238,15 @@ describe('RetryPlaceholders', () => { receivedAt: NOW + 15, }; const items: Array = [convo1a, convo1b, convo2a]; - await window.storage.put(STORAGE_KEY, items); + await storage.put(STORAGE_KEY, items); const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(3, placeholders.getCount()); await placeholders.findByConversationAndMarkOpened('conversation-id-1'); assert.strictEqual(3, placeholders.getCount()); - const firstSaveItems = window.storage.get(STORAGE_KEY); + const firstSaveItems = storage.get(STORAGE_KEY); assert.deepEqual( [ { @@ -267,7 +273,7 @@ describe('RetryPlaceholders', () => { await placeholders.findByConversationAndMarkOpened('conversation-id-2'); assert.strictEqual(4, placeholders.getCount()); - const secondSaveItems = window.storage.get(STORAGE_KEY); + const secondSaveItems = storage.get(STORAGE_KEY); assert.deepEqual( [ { @@ -307,10 +313,10 @@ describe('RetryPlaceholders', () => { sentAt: NOW - 11, }; const items: Array = [older, newer]; - await window.storage.put(STORAGE_KEY, items); + await storage.put(STORAGE_KEY, items); const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(2, placeholders.getCount()); assert.isUndefined( await placeholders.findByMessageAndRemove('conversation-id-1', sentAt) @@ -331,10 +337,10 @@ describe('RetryPlaceholders', () => { sentAt, }; const items: Array = [older, newer]; - await window.storage.put(STORAGE_KEY, items); + await storage.put(STORAGE_KEY, items); const placeholders = new RetryPlaceholders(); - placeholders.start(window.storage); + placeholders.start(storage); assert.strictEqual(2, placeholders.getCount()); assert.deepEqual( newer, diff --git a/ts/test-node/state/ducks/donations_test.ts b/ts/test-node/state/ducks/donations_test.ts index 5af94c1b48..54279876da 100644 --- a/ts/test-node/state/ducks/donations_test.ts +++ b/ts/test-node/state/ducks/donations_test.ts @@ -18,6 +18,21 @@ describe('donations duck', () => { const getEmptyRootState = (): StateType => rootReducer(undefined, noopAction()); + const storageMap = new Map(); + const storage = { + get: (key: string): unknown => storageMap.get(key), + put: async (key: string, value: unknown): Promise => { + storageMap.set(key, value); + }, + remove: async (key: string): Promise => { + storageMap.delete(key); + }, + }; + + beforeEach(() => { + storageMap.clear(); + }); + describe('applyDonationBadge thunk', () => { let sandbox: sinon.SinonSandbox; let myProfileChangedStub: sinon.SinonStub; @@ -36,9 +51,6 @@ describe('donations duck', () => { beforeEach(async () => { sandbox = sinon.createSandbox(); - // Clear storage for each test - await window.storage.remove('displayBadgesOnProfile'); - // Mock myProfileChanged by replacing the function directly myProfileChangedStub = sandbox.stub().returns(() => Promise.resolve()); originalMyProfileChanged = conversations.actions.myProfileChanged; @@ -49,8 +61,6 @@ describe('donations duck', () => { // Restore original myProfileChanged conversations.actions.myProfileChanged = originalMyProfileChanged; sandbox.restore(); - // Clean up storage - await window.storage.remove('displayBadgesOnProfile'); }); const createMeWithBadges = ( @@ -111,6 +121,7 @@ describe('donations duck', () => { badge, applyBadge, onComplete, + storage, })(dispatch, getState, null); }; @@ -139,7 +150,7 @@ describe('donations duck', () => { ]); // Verify storage was updated from false to true - assert.equal(window.storage.get('displayBadgesOnProfile'), true); + assert.equal(storage.get('displayBadgesOnProfile'), true); // Note: storageServiceUploadJob would be called here with // { reason: 'donation-badge-toggle' } but we can't spy on const exports @@ -162,7 +173,7 @@ describe('donations duck', () => { sinon.assert.notCalled(myProfileChangedStub); // Verify storage was written with false (even though unchanged) - assert.equal(window.storage.get('displayBadgesOnProfile'), false); + assert.equal(storage.get('displayBadgesOnProfile'), false); // Note: storageServiceUploadJob would not be called here sinon.assert.calledOnceWithExactly(onComplete); @@ -190,7 +201,7 @@ describe('donations duck', () => { ]); // Verify storage remains at true (no update needed) - assert.equal(window.storage.get('displayBadgesOnProfile'), true); + assert.equal(storage.get('displayBadgesOnProfile'), true); // Note: storageServiceUploadJob would not be called here (no change) sinon.assert.calledOnceWithExactly(onComplete); @@ -212,7 +223,7 @@ describe('donations duck', () => { assert.deepEqual(profileData.badges, []); // Verify storage was updated from true to false - assert.equal(window.storage.get('displayBadgesOnProfile'), false); + assert.equal(storage.get('displayBadgesOnProfile'), false); sinon.assert.calledOnceWithExactly(onComplete); }); @@ -238,7 +249,7 @@ describe('donations duck', () => { ]); // Verify storage remains at true (no update needed) - assert.equal(window.storage.get('displayBadgesOnProfile'), true); + assert.equal(storage.get('displayBadgesOnProfile'), true); sinon.assert.calledOnceWithExactly(onComplete); }); @@ -257,7 +268,7 @@ describe('donations duck', () => { sinon.assert.notCalled(myProfileChangedStub); // Verify storage remains at true (no update needed) - assert.equal(window.storage.get('displayBadgesOnProfile'), true); + assert.equal(storage.get('displayBadgesOnProfile'), true); sinon.assert.calledOnceWithExactly(onComplete); }); diff --git a/ts/test-node/state/ducks/preferredReactions_test.ts b/ts/test-node/state/ducks/preferredReactions_test.ts index e4b0897ddc..6be9b1dc57 100644 --- a/ts/test-node/state/ducks/preferredReactions_test.ts +++ b/ts/test-node/state/ducks/preferredReactions_test.ts @@ -14,6 +14,7 @@ import { reducer, } from '../../../state/ducks/preferredReactions.js'; import { EmojiSkinTone } from '../../../components/fun/data/emojis.js'; +import { itemStorage } from '../../../textsecure/Storage.js'; describe('preferred reactions duck', () => { const getEmptyRootState = (): StateType => @@ -254,7 +255,7 @@ describe('preferred reactions duck', () => { let oldConversationController: any; beforeEach(() => { - storagePutStub = sinonSandbox.stub(window.storage, 'put').resolves(); + storagePutStub = sinonSandbox.stub(itemStorage, 'put').resolves(); oldConversationController = window.ConversationController; diff --git a/ts/test-node/util/serverAlerts_test.ts b/ts/test-node/util/serverAlerts_test.ts index 84676befbe..e1e50febfb 100644 --- a/ts/test-node/util/serverAlerts_test.ts +++ b/ts/test-node/util/serverAlerts_test.ts @@ -2,10 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; -import { - getServerAlertToShow, - ServerAlert, -} from '../../util/handleServerAlerts.js'; +import { getServerAlertToShow } from '../../util/handleServerAlerts.js'; +import { ServerAlert } from '../../types/ServerAlert.js'; import { DAY, MONTH, WEEK } from '../../util/durations/index.js'; describe('serverAlerts', () => { diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index 1fef40e817..9d30fe1ef9 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -11,12 +11,19 @@ import { import { Readable } from 'node:stream'; import EventTarget from './EventTarget.js'; -import type { - UploadKeysType, - UploadKyberPreKeyType, - UploadPreKeyType, - UploadSignedPreKeyType, - WebAPIType, +import { + type UploadKeysType, + type UploadKyberPreKeyType, + type UploadPreKeyType, + type UploadSignedPreKeyType, + updateDeviceName, + getMyKeyCounts, + registerKeys, + startRegistration, + finishRegistration, + createAccount, + linkDevice, + authenticate, } from './WebAPI.js'; import type { CompatPreKeyType, @@ -73,6 +80,7 @@ import { isLinkAndSyncEnabled } from '../util/isLinkAndSyncEnabled.js'; import { getMessageQueueTime } from '../util/getMessageQueueTime.js'; import { canAttemptRemoteBackupDownload } from '../util/isBackupEnabled.js'; import { signalProtocolStore } from '../SignalProtocolStore.js'; +import { itemStorage } from './Storage.js'; const { isNumber, omit, orderBy } = lodash; @@ -189,7 +197,7 @@ function getNextKeyId( kind: ServiceIdKind, keys: StorageKeyByServiceIdKind ): number { - const id = window.storage.get(keys[kind]); + const id = itemStorage.get(keys[kind]); if (isNumber(id)) { return id; @@ -197,7 +205,7 @@ function getNextKeyId( // For PNI ids, start with existing ACI id if (kind === ServiceIdKind.PNI) { - return window.storage.get(keys[ServiceIdKind.ACI], STARTING_KEY_ID); + return itemStorage.get(keys[ServiceIdKind.ACI], STARTING_KEY_ID); } return STARTING_KEY_ID; @@ -249,7 +257,7 @@ export default class AccountManager extends EventTarget { pendingQueue?: PQueue; - constructor(private readonly server: WebAPIType) { + constructor() { super(); this.pending = Promise.resolve(); @@ -281,7 +289,7 @@ export default class AccountManager extends EventTarget { } async decryptDeviceName(base64: string): Promise { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const identityKey = signalProtocolStore.getIdentityKeyPair(ourAci); if (!identityKey) { throw new Error('decryptDeviceName: No identity key pair!'); @@ -309,15 +317,13 @@ export default class AccountManager extends EventTarget { } async maybeUpdateDeviceName(): Promise { - const isNameEncrypted = - window.textsecure.storage.user.getDeviceNameEncrypted(); + const isNameEncrypted = itemStorage.user.getDeviceNameEncrypted(); if (isNameEncrypted) { return; } - const { storage } = window.textsecure; - const deviceName = storage.user.getDeviceName(); + const deviceName = itemStorage.user.getDeviceName(); const identityKeyPair = signalProtocolStore.getIdentityKeyPair( - storage.user.getCheckedAci() + itemStorage.user.getCheckedAci() ); strictAssert( identityKeyPair !== undefined, @@ -326,13 +332,13 @@ export default class AccountManager extends EventTarget { const base64 = this.encryptDeviceName(deviceName || '', identityKeyPair); if (base64) { - await this.server.updateDeviceName(base64); - await window.textsecure.storage.user.setDeviceNameEncrypted(); + await updateDeviceName(base64); + await itemStorage.user.setDeviceNameEncrypted(); } } async deviceNameIsEncrypted(): Promise { - await window.textsecure.storage.user.setDeviceNameEncrypted(); + await itemStorage.user.setDeviceNameEncrypted(); } async registerSingleDevice( @@ -397,10 +403,8 @@ export default class AccountManager extends EventTarget { serviceIdKind: ServiceIdKind, count = PRE_KEY_GEN_BATCH_SIZE ): Promise> { - const ourServiceId = - window.textsecure.storage.user.getCheckedServiceId(serviceIdKind); + const ourServiceId = itemStorage.user.getCheckedServiceId(serviceIdKind); const logId = `AccountManager.generateNewPreKeys(${serviceIdKind})`; - const { storage } = window.textsecure; const startId = getNextKeyId(serviceIdKind, PRE_KEY_ID_KEY); log.info(`${logId}: Generating ${count} new keys starting at ${startId}`); @@ -418,7 +422,7 @@ export default class AccountManager extends EventTarget { await Promise.all([ signalProtocolStore.storePreKeys(ourServiceId, toSave), - storage.put(PRE_KEY_ID_KEY[serviceIdKind], startId + count), + itemStorage.put(PRE_KEY_ID_KEY[serviceIdKind], startId + count), ]); return toSave.map(key => ({ @@ -442,9 +446,7 @@ export default class AccountManager extends EventTarget { ); } - const { storage } = window.textsecure; - - const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind); + const ourServiceId = itemStorage.user.getCheckedServiceId(serviceIdKind); const identityKey = this.#getIdentityKeyOrThrow(ourServiceId); const toSave: Array> = []; @@ -469,7 +471,7 @@ export default class AccountManager extends EventTarget { await Promise.all([ signalProtocolStore.storeKyberPreKeys(ourServiceId, toSave), - storage.put(KYBER_KEY_ID_KEY[serviceIdKind], startId + count), + itemStorage.put(KYBER_KEY_ID_KEY[serviceIdKind], startId + count), ]); return toUpload; @@ -481,11 +483,11 @@ export default class AccountManager extends EventTarget { ): Promise { const logId = `maybeUpdateKeys(${serviceIdKind})`; await this.#queueTask(async () => { - const { storage } = window.textsecure; let identityKey: KeyPairType; try { - const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind); + const ourServiceId = + itemStorage.user.getCheckedServiceId(serviceIdKind); identityKey = this.#getIdentityKeyOrThrow(ourServiceId); } catch (error) { if (serviceIdKind === ServiceIdKind.PNI) { @@ -500,7 +502,7 @@ export default class AccountManager extends EventTarget { } const { count: preKeyCount, pqCount: kyberPreKeyCount } = - await this.server.getMyKeyCounts(serviceIdKind); + await getMyKeyCounts(serviceIdKind); let preKeys: Array | undefined; @@ -572,11 +574,11 @@ export default class AccountManager extends EventTarget { signedPreKey, }; - await this.server.registerKeys(toUpload, serviceIdKind); + await registerKeys(toUpload, serviceIdKind); await this._confirmKeys(toUpload, serviceIdKind); const { count: updatedPreKeyCount, pqCount: updatedKyberPreKeyCount } = - await this.server.getMyKeyCounts(serviceIdKind); + await getMyKeyCounts(serviceIdKind); log.info( `${logId}: Successfully updated; ` + `server prekey count: ${updatedPreKeyCount}, ` + @@ -591,11 +593,11 @@ export default class AccountManager extends EventTarget { } areKeysOutOfDate(serviceIdKind: ServiceIdKind): boolean { - const signedPreKeyTime = window.storage.get( + const signedPreKeyTime = itemStorage.get( SIGNED_PRE_KEY_UPDATE_TIME_KEY[serviceIdKind], 0 ); - const lastResortKeyTime = window.storage.get( + const lastResortKeyTime = itemStorage.get( LAST_RESORT_KEY_UPDATE_TIME_KEY[serviceIdKind], 0 ); @@ -626,7 +628,7 @@ export default class AccountManager extends EventTarget { const key = await generateSignedPreKey(identityKey, signedKeyId); log.info(`${logId}: Saving new signed prekey`, key.keyId); - await window.textsecure.storage.put( + await itemStorage.put( SIGNED_PRE_KEY_ID_KEY[serviceIdKind], signedKeyId + 1 ); @@ -638,8 +640,7 @@ export default class AccountManager extends EventTarget { serviceIdKind: ServiceIdKind, forceUpdate = false ): Promise { - const ourServiceId = - window.textsecure.storage.user.getCheckedServiceId(serviceIdKind); + const ourServiceId = itemStorage.user.getCheckedServiceId(serviceIdKind); const identityKey = this.#getIdentityKeyOrThrow(ourServiceId); const logId = `AccountManager.maybeUpdateSignedPreKey(${serviceIdKind}, ${ourServiceId})`; @@ -657,12 +658,12 @@ export default class AccountManager extends EventTarget { `${logId}: ${confirmedKeys.length} confirmed keys, ` + `most recent was created ${lastUpdate}. No need to update.` ); - const existing = window.storage.get( + const existing = itemStorage.get( SIGNED_PRE_KEY_UPDATE_TIME_KEY[serviceIdKind] ); if (lastUpdate && !existing) { log.warn(`${logId}: Updating last update time to ${lastUpdate}`); - await window.storage.put( + await itemStorage.put( SIGNED_PRE_KEY_UPDATE_TIME_KEY[serviceIdKind], lastUpdate ); @@ -699,10 +700,7 @@ export default class AccountManager extends EventTarget { const record = await generateKyberPreKey(identityKey, keyId); log.info(`${logId}: Saving new last resort prekey`, keyId); - await window.textsecure.storage.put( - KYBER_KEY_ID_KEY[serviceIdKind], - kyberKeyId + 1 - ); + await itemStorage.put(KYBER_KEY_ID_KEY[serviceIdKind], kyberKeyId + 1); return record; } @@ -711,8 +709,7 @@ export default class AccountManager extends EventTarget { serviceIdKind: ServiceIdKind, forceUpdate = false ): Promise { - const ourServiceId = - window.textsecure.storage.user.getCheckedServiceId(serviceIdKind); + const ourServiceId = itemStorage.user.getCheckedServiceId(serviceIdKind); const identityKey = this.#getIdentityKeyOrThrow(ourServiceId); const logId = `maybeUpdateLastResortKyberKey(${serviceIdKind}, ${ourServiceId})`; @@ -732,12 +729,12 @@ export default class AccountManager extends EventTarget { `${logId}: ${confirmedKeys.length} confirmed keys, ` + `most recent was created ${lastUpdate}. No need to update.` ); - const existing = window.storage.get( + const existing = itemStorage.get( LAST_RESORT_KEY_UPDATE_TIME_KEY[serviceIdKind] ); if (lastUpdate && !existing) { log.warn(`${logId}: Updating last update time to ${lastUpdate}`); - await window.storage.put( + await itemStorage.put( LAST_RESORT_KEY_UPDATE_TIME_KEY[serviceIdKind], lastUpdate ); @@ -759,8 +756,7 @@ export default class AccountManager extends EventTarget { // Exposed only for tests async _cleanSignedPreKeys(serviceIdKind: ServiceIdKind): Promise { - const ourServiceId = - window.textsecure.storage.user.getCheckedServiceId(serviceIdKind); + const ourServiceId = itemStorage.user.getCheckedServiceId(serviceIdKind); const logId = `AccountManager.cleanSignedPreKeys(${serviceIdKind})`; const allKeys = signalProtocolStore.loadSignedPreKeys(ourServiceId); @@ -811,8 +807,7 @@ export default class AccountManager extends EventTarget { // Exposed only for tests async _cleanLastResortKeys(serviceIdKind: ServiceIdKind): Promise { - const ourServiceId = - window.textsecure.storage.user.getCheckedServiceId(serviceIdKind); + const ourServiceId = itemStorage.user.getCheckedServiceId(serviceIdKind); const logId = `AccountManager.cleanLastResortKeys(${serviceIdKind})`; const allKeys = signalProtocolStore.loadKyberPreKeys(ourServiceId, { @@ -867,8 +862,7 @@ export default class AccountManager extends EventTarget { async _cleanPreKeys(serviceIdKind: ServiceIdKind): Promise { const logId = `AccountManager.cleanPreKeys(${serviceIdKind})`; - const ourServiceId = - window.textsecure.storage.user.getCheckedServiceId(serviceIdKind); + const ourServiceId = itemStorage.user.getCheckedServiceId(serviceIdKind); const preKeys = signalProtocolStore.loadPreKeys(ourServiceId); const toDelete: Array = []; @@ -894,8 +888,7 @@ export default class AccountManager extends EventTarget { async _cleanKyberPreKeys(serviceIdKind: ServiceIdKind): Promise { const logId = `AccountManager.cleanKyberPreKeys(${serviceIdKind})`; - const ourServiceId = - window.textsecure.storage.user.getCheckedServiceId(serviceIdKind); + const ourServiceId = itemStorage.user.getCheckedServiceId(serviceIdKind); const preKeys = signalProtocolStore.loadKyberPreKeys(ourServiceId, { isLastResort: false, @@ -923,11 +916,11 @@ export default class AccountManager extends EventTarget { async #createAccount(options: CreateAccountOptionsType): Promise { this.dispatchEvent(new Event('startRegistration')); - const registrationBaton = this.server.startRegistration(); + const registrationBaton = startRegistration(); try { await this.#doCreateAccount(options); } finally { - this.server.finishRegistration(registrationBaton); + finishRegistration(registrationBaton); } await this.#registrationDone(); } @@ -952,15 +945,14 @@ export default class AccountManager extends EventTarget { 'Either master key or AEP is necessary for registration' ); - const { storage } = window.textsecure; let password = Bytes.toBase64(getRandomBytes(16)); password = password.substring(0, password.length - 2); const registrationId = generateRegistrationId(); const pniRegistrationId = generateRegistrationId(); - const previousNumber = storage.user.getNumber(); - const previousACI = storage.user.getAci(); - const previousPNI = storage.user.getPni(); + const previousNumber = itemStorage.user.getNumber(); + const previousACI = itemStorage.user.getAci(); + const previousPNI = itemStorage.user.getPni(); log.info( `createAccount: Number is ${number}, password has length: ${ @@ -1022,13 +1014,13 @@ export default class AccountManager extends EventTarget { if (previousUuids.length > 0) { await Promise.all([ - storage.put( + itemStorage.put( 'identityKeyMap', - omit(storage.get('identityKeyMap') || {}, previousUuids) + omit(itemStorage.get('identityKeyMap') || {}, previousUuids) ), - storage.put( + itemStorage.put( 'registrationIdMap', - omit(storage.get('registrationIdMap') || {}, previousUuids) + omit(itemStorage.get('registrationIdMap') || {}, previousUuids) ), ]); } @@ -1066,7 +1058,7 @@ export default class AccountManager extends EventTarget { }; if (options.type === AccountType.Primary) { - const response = await this.server.createAccount({ + const response = await createAccount({ number, code: verificationCode, newPassword: password, @@ -1095,7 +1087,7 @@ export default class AccountManager extends EventTarget { ); await this.deviceNameIsEncrypted(); - const response = await this.server.linkDevice({ + const response = await linkDevice({ number, verificationCode, encryptedDeviceName, @@ -1135,10 +1127,13 @@ export default class AccountManager extends EventTarget { if (shouldDownloadBackup && cleanStart) { if (options.type === AccountType.Linked && options.ephemeralBackupKey) { log.info('createAccount: setting ephemeral key'); - await storage.put('backupEphemeralKey', options.ephemeralBackupKey); + await itemStorage.put('backupEphemeralKey', options.ephemeralBackupKey); } log.info('createAccount: setting backup download path'); - await storage.put('backupDownloadPath', getRelativePath(createName())); + await itemStorage.put( + 'backupDownloadPath', + getRelativePath(createName()) + ); } // `setCredentials` needs to be called @@ -1147,7 +1142,7 @@ export default class AccountManager extends EventTarget { // initializes the conversation for the given number (our number) which // calls out to the user storage API to get the stored UUID and number // information. - await storage.user.setCredentials({ + await itemStorage.user.setCredentials({ aci: ourAci, pni: ourPni, number, @@ -1156,7 +1151,7 @@ export default class AccountManager extends EventTarget { password, }); - await this.server.authenticate(storage.user.getWebAPICredentials()); + await authenticate(itemStorage.user.getWebAPICredentials()); // This needs to be done very early, because it changes how things are saved in the // database. Your identity, for example, in the saveIdentityWithAttributes call @@ -1188,7 +1183,7 @@ export default class AccountManager extends EventTarget { }), ]); - const identityKeyMap = storage.get('identityKeyMap') || {}; + const identityKeyMap = itemStorage.get('identityKeyMap') || {}; identityKeyMap[ourAci] = { pubKey: aciKeyPair.publicKey.serialize(), @@ -1199,12 +1194,12 @@ export default class AccountManager extends EventTarget { privKey: pniKeyPair.privateKey.serialize(), }; - const registrationIdMap = storage.get('registrationIdMap') || {}; + const registrationIdMap = itemStorage.get('registrationIdMap') || {}; registrationIdMap[ourAci] = registrationId; registrationIdMap[ourPni] = pniRegistrationId; - await storage.put('identityKeyMap', identityKeyMap); - await storage.put('registrationIdMap', registrationIdMap); + await itemStorage.put('identityKeyMap', identityKeyMap); + await itemStorage.put('registrationIdMap', registrationIdMap); await ourProfileKeyService.set(profileKey); const me = window.ConversationController.getOurConversationOrThrow(); @@ -1214,10 +1209,10 @@ export default class AccountManager extends EventTarget { await me.updateVerified(); if (userAgent) { - await storage.put('userAgent', userAgent); + await itemStorage.put('userAgent', userAgent); } if (accountEntropyPool) { - await storage.put('accountEntropyPool', accountEntropyPool); + await itemStorage.put('accountEntropyPool', accountEntropyPool); } else { log.warn('createAccount: accountEntropyPool was missing!'); } @@ -1227,18 +1222,18 @@ export default class AccountManager extends EventTarget { derivedMasterKey = deriveMasterKey(accountEntropyPool); } if (Bytes.isNotEmpty(mediaRootBackupKey)) { - await storage.put('backupMediaRootKey', mediaRootBackupKey); + await itemStorage.put('backupMediaRootKey', mediaRootBackupKey); } - await storage.put('masterKey', Bytes.toBase64(derivedMasterKey)); - await storage.put( + await itemStorage.put('masterKey', Bytes.toBase64(derivedMasterKey)); + await itemStorage.put( 'storageKey', Bytes.toBase64(deriveStorageServiceKey(derivedMasterKey)) ); - await storage.put('read-receipt-setting', Boolean(readReceipts)); + await itemStorage.put('read-receipt-setting', Boolean(readReceipts)); const regionCode = getRegionCodeForNumber(number); - await storage.put('regionCode', regionCode); + await itemStorage.put('regionCode', regionCode); await signalProtocolStore.hydrateCaches(); await signalProtocolStore.storeSignedPreKey( @@ -1276,7 +1271,7 @@ export default class AccountManager extends EventTarget { const uploadKeys = async (kind: ServiceIdKind) => { try { const keys = await this._generateSingleUseKeys(kind); - await this.server.registerKeys(keys, kind); + await registerKeys(keys, kind); } catch (error) { if (kind === ServiceIdKind.PNI) { log.error( @@ -1311,8 +1306,7 @@ export default class AccountManager extends EventTarget { }>, serviceIdKind: ServiceIdKind ): Promise { - const ourServiceId = - window.textsecure.storage.user.getCheckedServiceId(serviceIdKind); + const ourServiceId = itemStorage.user.getCheckedServiceId(serviceIdKind); const logId = `AccountManager.confirmKeys(${serviceIdKind})`; const updatedAt = Date.now(); @@ -1322,7 +1316,7 @@ export default class AccountManager extends EventTarget { ourServiceId, signedPreKey.keyId ); - await window.storage.put( + await itemStorage.put( SIGNED_PRE_KEY_UPDATE_TIME_KEY[serviceIdKind], updatedAt ); @@ -1339,7 +1333,7 @@ export default class AccountManager extends EventTarget { ourServiceId, pqLastResortPreKey.keyId ); - await window.storage.put( + await itemStorage.put( LAST_RESORT_KEY_UPDATE_TIME_KEY[serviceIdKind], updatedAt ); @@ -1353,8 +1347,7 @@ export default class AccountManager extends EventTarget { serviceIdKind: ServiceIdKind, count = PRE_KEY_GEN_BATCH_SIZE ): Promise { - const ourServiceId = - window.textsecure.storage.user.getCheckedServiceId(serviceIdKind); + const ourServiceId = itemStorage.user.getCheckedServiceId(serviceIdKind); const logId = `AccountManager.generateKeys(${serviceIdKind}, ${ourServiceId})`; const preKeys = await this.#generateNewPreKeys(serviceIdKind, count); @@ -1387,9 +1380,8 @@ export default class AccountManager extends EventTarget { keyMaterial?: PniKeyMaterialType ): Promise { const logId = `AccountManager.setPni(${pni})`; - const { storage } = window.textsecure; - const oldPni = storage.user.getPni(); + const oldPni = itemStorage.user.getPni(); if (oldPni === pni && !keyMaterial) { return; } @@ -1401,7 +1393,7 @@ export default class AccountManager extends EventTarget { await window.ConversationController.clearShareMyPhoneNumber(); } - await storage.user.setPni(pni); + await itemStorage.user.setPni(pni); if (keyMaterial) { await signalProtocolStore.updateOurPniKeyMaterial(pni, keyMaterial); @@ -1423,7 +1415,7 @@ export default class AccountManager extends EventTarget { ); // PNI has changed and credentials are no longer valid - await storage.put('groupCredentials', []); + await itemStorage.put('groupCredentials', []); } else { log.warn(`${logId}: no key material`); } diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 5e7e72a020..e57e82a5c1 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -86,7 +86,7 @@ import type { EventHandler } from './EventTarget.js'; import EventTarget from './EventTarget.js'; import type { IncomingWebSocketRequest } from './WebsocketResources.js'; import { ServerRequestType } from './WebsocketResources.js'; -import type { Storage } from './Storage.js'; +import { type Storage } from './Storage.js'; import { WarnOnlyError } from './Errors.js'; import * as Bytes from '../Bytes.js'; import type { @@ -3332,7 +3332,7 @@ export default class MessageReceiver signedPreKey, registrationId, }); - await window.storage.user.setNumber(newE164); + await this.#storage.user.setNumber(newE164); } async #handleStickerPackOperation( @@ -3862,7 +3862,7 @@ export default class MessageReceiver await this.#dispatchAndWait(logId, contactSync); } - // This function calls applyMessageRequestResponse before setting window.storage so + // This function calls applyMessageRequestResponse before setting storage so // proper before/after logic can be applied within that function. async #handleBlocked( envelope: ProcessedEnvelope, diff --git a/ts/textsecure/OutgoingMessage.ts b/ts/textsecure/OutgoingMessage.ts index a3f21d9d50..65bff583e0 100644 --- a/ts/textsecure/OutgoingMessage.ts +++ b/ts/textsecure/OutgoingMessage.ts @@ -23,7 +23,13 @@ import { UnidentifiedSenderMessageContent, } from '@signalapp/libsignal-client'; -import type { WebAPIType, MessageType } from './WebAPI.js'; +import { + sendMessages, + sendMessagesUnauth, + getKeysForServiceId as doGetKeysForServiceId, + getKeysForServiceIdUnauth, +} from './WebAPI.js'; +import type { MessageType } from './WebAPI.js'; import type { SendMetadataType, SendOptionsType } from './SendMessage.js'; import { OutgoingIdentityKeyError, @@ -46,6 +52,7 @@ import type { GroupSendToken } from '../types/GroupSendEndorsements.js'; import { isSignalServiceId } from '../util/isSignalConversation.js'; import * as Bytes from '../Bytes.js'; import { signalProtocolStore } from '../SignalProtocolStore.js'; +import { itemStorage } from './Storage.js'; const { reject } = lodash; @@ -115,8 +122,6 @@ export function padMessage(messageBuffer: Uint8Array): Uint8Array { } export default class OutgoingMessage { - server: WebAPIType; - timestamp: number; serviceIds: ReadonlyArray; @@ -161,7 +166,6 @@ export default class OutgoingMessage { message, options, sendLogCallback, - server, story, timestamp, urgent, @@ -173,7 +177,6 @@ export default class OutgoingMessage { message: Proto.Content | Proto.DataMessage | PlaintextContent; options?: OutgoingMessageOptionsType; sendLogCallback?: SendLogCallbackType; - server: WebAPIType; story?: boolean; timestamp: number; urgent: boolean; @@ -186,7 +189,6 @@ export default class OutgoingMessage { this.message = message; } - this.server = server; this.timestamp = timestamp; this.serviceIds = serviceIds; this.contentHint = contentHint; @@ -274,7 +276,7 @@ export default class OutgoingMessage { recurse?: boolean ): () => Promise { return async () => { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const deviceIds = await signalProtocolStore.getDeviceIds({ ourServiceId: ourAci, serviceId, @@ -304,7 +306,7 @@ export default class OutgoingMessage { const { accessKeyFailed } = await getKeysForServiceId( serviceId, - this.server, + { getKeysForServiceId: doGetKeysForServiceId, getKeysForServiceIdUnauth }, updateDevices ?? null, accessKey, null @@ -329,7 +331,7 @@ export default class OutgoingMessage { let promise; if (accessKey != null || groupSendToken != null) { - promise = this.server.sendMessagesUnauth(serviceId, jsonData, timestamp, { + promise = sendMessagesUnauth(serviceId, jsonData, timestamp, { accessKey, groupSendToken, online: this.online, @@ -337,7 +339,7 @@ export default class OutgoingMessage { urgent: this.urgent, }); } else { - promise = this.server.sendMessages(serviceId, jsonData, timestamp, { + promise = sendMessages(serviceId, jsonData, timestamp, { online: this.online, story: this.story, urgent: this.urgent, @@ -429,9 +431,9 @@ export default class OutgoingMessage { senderCertificate != null; // We don't send to ourselves unless sealedSender is enabled - const ourNumber = window.textsecure.storage.user.getNumber(); - const ourAci = window.textsecure.storage.user.getCheckedAci(); - const ourDeviceId = window.textsecure.storage.user.getDeviceId(); + const ourNumber = itemStorage.user.getNumber(); + const ourAci = itemStorage.user.getCheckedAci(); + const ourDeviceId = itemStorage.user.getDeviceId(); if ((serviceId === ourNumber || serviceId === ourAci) && !sealedSender) { deviceIds = reject( deviceIds, @@ -684,7 +686,7 @@ export default class OutgoingMessage { serviceId: ServiceIdString, deviceIdsToRemove: Array ): Promise { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); await Promise.all( deviceIdsToRemove.map(async deviceId => { @@ -706,7 +708,7 @@ export default class OutgoingMessage { } try { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const deviceIds = await signalProtocolStore.getDeviceIds({ ourServiceId: ourAci, serviceId, diff --git a/ts/textsecure/Provisioner.ts b/ts/textsecure/Provisioner.ts index d8be501006..c8a8419704 100644 --- a/ts/textsecure/Provisioner.ts +++ b/ts/textsecure/Provisioner.ts @@ -31,7 +31,7 @@ import { ServerRequestType, } from './WebsocketResources.js'; import { ConnectTimeoutError } from './Errors.js'; -import { type WebAPIType } from './WebAPI.js'; +import type { getProvisioningResource } from './WebAPI.js'; const log = createLogger('Provisioner'); @@ -45,7 +45,7 @@ export enum EventKind { } export type ProvisionerOptionsType = Readonly<{ - server: WebAPIType; + server: { getProvisioningResource: typeof getProvisioningResource }; }>; export type EnvelopeType = ProvisionDecryptResult; @@ -107,7 +107,7 @@ const QR_CODE_TIMEOUTS = [10 * SECOND, 20 * SECOND, 30 * SECOND, 60 * SECOND]; export class Provisioner { readonly #subscribers = new Set(); - readonly #server: WebAPIType; + readonly #server: { getProvisioningResource: typeof getProvisioningResource }; readonly #retryBackOff = new BackOff(FIBONACCI_TIMEOUTS); #sockets: Array = []; diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index e890a006b6..8b984c8bfb 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -38,19 +38,8 @@ import { toPniObject, toServiceIdObject, } from '../util/ServiceId.js'; -import type { - ChallengeType, - GetGroupLogOptionsType, - GroupCredentialsType, - GroupLogResponseType, - WebAPIType, -} from './WebAPI.js'; import createTaskWithTimeout from './TaskWithTimeout.js'; -import type { - CallbackResultType, - StorageServiceCallOptionsType, - StorageServiceCredentials, -} from './Types.d.ts'; +import type { CallbackResultType } from './Types.d.ts'; import type { SerializedCertificateType, SendLogCallbackType, @@ -67,10 +56,6 @@ import { BodyRange } from '../types/BodyRange.js'; import { HTTPError } from '../types/HTTPError.js'; import type { RawBodyRange } from '../types/BodyRange.js'; import type { StoryContextType } from '../types/Util.js'; -import type { - LinkPreviewImage, - LinkPreviewMetadata, -} from '../linkPreviews/linkPreviewFetch.js'; import { concat, isEmpty } from '../util/iterables.js'; import type { SendTypesType } from '../util/handleMessageSend.js'; import { shouldSaveProto, sendTypesEnum } from '../util/handleMessageSend.js'; @@ -110,6 +95,7 @@ import { import { MAX_MESSAGE_COUNT } from '../util/deleteForMe.types.js'; import { isProtoBinaryEncodingEnabled } from '../util/isProtoBinaryEncodingEnabled.js'; import type { GroupSendToken } from '../types/GroupSendEndorsements.js'; +import { itemStorage } from './Storage.js'; const log = createLogger('SendMessage'); @@ -671,12 +657,12 @@ function addPniSignatureMessageToProto({ }; } -export default class MessageSender { +export class MessageSender { pendingMessages: { [id: string]: PQueue; }; - constructor(public readonly server: WebAPIType) { + constructor() { this.pendingMessages = {}; } @@ -953,14 +939,14 @@ export default class MessageSender { ); } - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const groupMembers = groupV2?.members || []; const blockedIdentifiers = new Set( concat( - window.storage.blocked.getBlockedServiceIds(), - window.storage.blocked.getBlockedNumbers() + itemStorage.blocked.getBlockedServiceIds(), + itemStorage.blocked.getBlockedNumbers() ) ); @@ -1100,7 +1086,6 @@ export default class MessageSender { message: proto, options, sendLogCallback, - server: this.server, story, timestamp, urgent, @@ -1310,7 +1295,7 @@ export default class MessageSender { storyMessage?: Proto.StoryMessage; storyMessageRecipients?: ReadonlyArray; }>): Promise { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const sentMessage = new Proto.SyncMessage.Sent(); sentMessage.timestamp = Long.fromNumber(timestamp); @@ -1398,7 +1383,7 @@ export default class MessageSender { } static getRequestBlockSyncMessage(): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const request = new Proto.SyncMessage.Request(); request.type = Proto.SyncMessage.Request.Type.BLOCKED; @@ -1420,7 +1405,7 @@ export default class MessageSender { } static getRequestConfigurationSyncMessage(): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const request = new Proto.SyncMessage.Request(); request.type = Proto.SyncMessage.Request.Type.CONFIGURATION; @@ -1442,7 +1427,7 @@ export default class MessageSender { } static getRequestContactSyncMessage(): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const request = new Proto.SyncMessage.Request(); request.type = Proto.SyncMessage.Request.Type.CONTACTS; @@ -1464,7 +1449,7 @@ export default class MessageSender { } static getFetchManifestSyncMessage(): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const fetchLatest = new Proto.SyncMessage.FetchLatest(); fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST; @@ -1487,7 +1472,7 @@ export default class MessageSender { } static getFetchLocalProfileSyncMessage(): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const fetchLatest = new Proto.SyncMessage.FetchLatest(); fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.LOCAL_PROFILE; @@ -1510,7 +1495,7 @@ export default class MessageSender { } static getRequestKeySyncMessage(): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const request = new Proto.SyncMessage.Request(); request.type = Proto.SyncMessage.Request.Type.KEYS; @@ -1535,7 +1520,7 @@ export default class MessageSender { static getDeleteForMeSyncMessage( data: DeleteForMeSyncEventData ): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const deleteForMe = new Proto.SyncMessage.DeleteForMe(); const messageDeletes: Map< @@ -1632,7 +1617,7 @@ export default class MessageSender { targetConversation: ConversationIdentifier, targetMessage: AddressableMessage ): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const syncMessage = this.createSyncMessage(); syncMessage.attachmentBackfillRequest = { @@ -1658,7 +1643,7 @@ export default class MessageSender { static getClearCallHistoryMessage( latestCall: CallHistoryDetails ): SingleProtoJobData { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const callLogEvent = new Proto.SyncMessage.CallLogEvent({ type: Proto.SyncMessage.CallLogEvent.Type.CLEAR, timestamp: Long.fromNumber(latestCall.timestamp), @@ -1685,7 +1670,7 @@ export default class MessageSender { } static getDeleteCallEvent(callDetails: CallDetails): SingleProtoJobData { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const { mode } = callDetails; let status; if (mode === CallMode.Adhoc) { @@ -1728,7 +1713,7 @@ export default class MessageSender { }>, options?: Readonly ): Promise { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const syncMessage = MessageSender.createSyncMessage(); syncMessage.read = []; @@ -1761,7 +1746,7 @@ export default class MessageSender { }>, options?: SendOptionsType ): Promise { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const syncMessage = MessageSender.createSyncMessage(); syncMessage.viewed = views.map( @@ -1803,7 +1788,7 @@ export default class MessageSender { throw new Error('syncViewOnceOpen: Missing senderAci'); } - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const syncMessage = MessageSender.createSyncMessage(); @@ -1836,7 +1821,7 @@ export default class MessageSender { groupIds: Array; }> ): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const syncMessage = MessageSender.createSyncMessage(); @@ -1874,7 +1859,7 @@ export default class MessageSender { type: number; }> ): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const syncMessage = MessageSender.createSyncMessage(); @@ -1916,7 +1901,7 @@ export default class MessageSender { installed: boolean; }> ): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); const ENUM = Proto.SyncMessage.StickerPackOperation.Type; const packOperations = operations.map(item => { @@ -1954,7 +1939,7 @@ export default class MessageSender { state: number, identityKey: Readonly ): SingleProtoJobData { - const myAci = window.textsecure.storage.user.getCheckedAci(); + const myAci = itemStorage.user.getCheckedAci(); if (!destinationE164 && !destinationAci) { throw new Error('syncVerification: Neither e164 nor UUID were provided'); @@ -2224,8 +2209,8 @@ export default class MessageSender { timestamp: number; urgent: boolean; }>): Promise { - const myE164 = window.textsecure.storage.user.getNumber(); - const myAci = window.textsecure.storage.user.getAci(); + const myE164 = itemStorage.user.getNumber(); + const myAci = itemStorage.user.getAci(); const serviceIds = recipients.filter(id => id !== myE164 && id !== myAci); if (serviceIds.length === 0) { @@ -2282,9 +2267,9 @@ export default class MessageSender { timestamp, }: { throwIfNotInDatabase?: boolean; timestamp: number } ): Promise { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const ourDeviceId = parseIntOrThrow( - window.textsecure.storage.user.getDeviceId(), + itemStorage.user.getDeviceId(), 'getSenderKeyDistributionMessage' ); @@ -2384,126 +2369,10 @@ export default class MessageSender { urgent, }); } - - // Simple pass-throughs - - // Note: instead of updating these functions, or adding new ones, remove these and go - // directly to window.textsecure.messaging.server. - - async getAvatar(path: string): Promise> { - return this.server.getAvatar(path); - } - - async getSticker( - packId: string, - stickerId: number - ): Promise> { - return this.server.getSticker(packId, stickerId); - } - - async getStickerPackManifest( - packId: string - ): Promise> { - return this.server.getStickerPackManifest(packId); - } - - async createGroup( - group: Readonly, - options: Readonly - ): Promise { - return this.server.createGroup(group, options); - } - - async uploadGroupAvatar( - avatar: Readonly, - options: Readonly - ): Promise { - return this.server.uploadGroupAvatar(avatar, options); - } - - async getGroup( - options: Readonly - ): Promise { - return this.server.getGroup(options); - } - - async getGroupFromLink( - groupInviteLink: string | undefined, - auth: Readonly - ): Promise { - return this.server.getGroupFromLink(groupInviteLink, auth); - } - - async getGroupLog( - options: GetGroupLogOptionsType, - credentials: GroupCredentialsType - ): Promise { - return this.server.getGroupLog(options, credentials); - } - - async getGroupAvatar(key: string): Promise { - return this.server.getGroupAvatar(key); - } - - async modifyGroup( - changes: Readonly, - options: Readonly, - inviteLinkBase64?: string - ): Promise { - return this.server.modifyGroup(changes, options, inviteLinkBase64); - } - - async fetchLinkPreviewMetadata( - href: string, - abortSignal: AbortSignal - ): Promise { - return this.server.fetchLinkPreviewMetadata(href, abortSignal); - } - - async fetchLinkPreviewImage( - href: string, - abortSignal: AbortSignal - ): Promise { - return this.server.fetchLinkPreviewImage(href, abortSignal); - } - - async getStorageCredentials(): Promise { - return this.server.getStorageCredentials(); - } - - async getStorageManifest( - options: Readonly - ): Promise { - return this.server.getStorageManifest(options); - } - - async getStorageRecords( - data: Readonly, - options: Readonly - ): Promise { - return this.server.getStorageRecords(data, options); - } - - async modifyStorageRecords( - data: Readonly, - options: Readonly - ): Promise { - return this.server.modifyStorageRecords(data, options); - } - - async getGroupMembershipToken( - options: Readonly - ): Promise { - return this.server.getExternalGroupCredential(options); - } - - public async sendChallengeResponse( - challengeResponse: Readonly - ): Promise { - return this.server.sendChallengeResponse(challengeResponse); - } } +export const messageSender = new MessageSender(); + // Helpers function toAddressableMessage(message: AddressableMessage) { diff --git a/ts/textsecure/SocketManager.ts b/ts/textsecure/SocketManager.ts index 7301f11b59..ec3925bfda 100644 --- a/ts/textsecure/SocketManager.ts +++ b/ts/textsecure/SocketManager.ts @@ -46,7 +46,7 @@ import WebSocketResource, { import { ConnectTimeoutError } from './Errors.js'; import type { IRequestHandler, WebAPICredentials } from './Types.d.ts'; import { connect as connectWebSocket } from './WebSocket.js'; -import { type ServerAlert } from '../util/handleServerAlerts.js'; +import type { ServerAlert } from '../types/ServerAlert.js'; import { getUserLanguages } from '../util/userLanguages.js'; const log = createLogger('SocketManager'); @@ -67,7 +67,6 @@ export type SocketManagerOptions = Readonly<{ certificateAuthority: string; version: string; proxyUrl?: string; - hasStoriesDisabled: boolean; }>; type SocketStatusUpdate = { status: SocketStatus }; @@ -114,7 +113,7 @@ export class SocketManager extends EventListener { #isNavigatorOffline = false; #privIsOnline: boolean | undefined; #expirationReason: SocketExpirationReason | undefined; - #hasStoriesDisabled: boolean; + #hasStoriesDisabled: boolean | undefined; #reconnectController: AbortController | undefined; #envelopeCount = 0; @@ -123,8 +122,6 @@ export class SocketManager extends EventListener { private readonly options: SocketManagerOptions ) { super(); - - this.#hasStoriesDisabled = options.hasStoriesDisabled; } public getStatus(): SocketStatuses { @@ -211,7 +208,7 @@ export class SocketManager extends EventListener { onReceivedAlerts: (alerts: Array) => { this.emit('serverAlerts', alerts); }, - receiveStories: !this.#hasStoriesDisabled, + receiveStories: this.#hasStoriesDisabled === false, userLanguages, keepalive: { path: '/v1/keepalive' }, }); diff --git a/ts/textsecure/Storage.ts b/ts/textsecure/Storage.ts index 44ed1506c2..9e5e42cedc 100644 --- a/ts/textsecure/Storage.ts +++ b/ts/textsecure/Storage.ts @@ -32,8 +32,6 @@ export class Storage implements StorageInterface { constructor() { this.user = new User(this); this.blocked = new Blocked(this); - - window.storage = this; } // `StorageInterface` implementation @@ -140,3 +138,5 @@ export class Storage implements StorageInterface { callbacks.forEach(callback => callback()); } } + +export const itemStorage = new Storage(); diff --git a/ts/textsecure/UpdateKeysListener.ts b/ts/textsecure/UpdateKeysListener.ts index c397d3f7c8..0d23a2735b 100644 --- a/ts/textsecure/UpdateKeysListener.ts +++ b/ts/textsecure/UpdateKeysListener.ts @@ -8,6 +8,8 @@ import { ServiceIdKind } from '../types/ServiceId.js'; import { createLogger } from '../logging/log.js'; import * as Errors from '../types/errors.js'; import { HTTPError } from '../types/HTTPError.js'; +import { isOnline } from './WebAPI.js'; +import { itemStorage } from './Storage.js'; const log = createLogger('UpdateKeysListener'); @@ -25,12 +27,12 @@ export class UpdateKeysListener { protected scheduleUpdateForNow(): void { const now = Date.now(); - void window.textsecure.storage.put(UPDATE_TIME_STORAGE_KEY, now); + void itemStorage.put(UPDATE_TIME_STORAGE_KEY, now); } protected setTimeoutForNextRun(): void { const now = Date.now(); - const time = window.textsecure.storage.get(UPDATE_TIME_STORAGE_KEY, now); + const time = itemStorage.get(UPDATE_TIME_STORAGE_KEY, now); log.info('Next update scheduled for', new Date(time).toISOString()); @@ -46,7 +48,7 @@ export class UpdateKeysListener { #scheduleNextUpdate(): void { const now = Date.now(); const nextTime = now + UPDATE_INTERVAL; - void window.textsecure.storage.put(UPDATE_TIME_STORAGE_KEY, nextTime); + void itemStorage.put(UPDATE_TIME_STORAGE_KEY, nextTime); } async #run(): Promise { @@ -89,7 +91,7 @@ export class UpdateKeysListener { } #runWhenOnline() { - if (window.textsecure.server?.isOnline()) { + if (isOnline()) { void this.#run(); } else { log.info('We are offline; will update keys when we are next online'); diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index 31639f798f..f3cc8feca4 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -72,7 +72,6 @@ import { CDSI } from './cds/CDSI.js'; import { SignalService as Proto } from '../protobuf/index.js'; import { isEnabled as isRemoteConfigEnabled } from '../RemoteConfig.js'; -import type MessageSender from './SendMessage.js'; import type { WebAPICredentials, IRequestHandler, @@ -98,7 +97,7 @@ import type { } from '../services/profiles.js'; import { ToastType } from '../types/Toast.js'; import { isProduction } from '../util/version.js'; -import type { ServerAlert } from '../util/handleServerAlerts.js'; +import type { ServerAlert } from '../types/ServerAlert.js'; import { isAbortError } from '../util/isAbortError.js'; import { missingCaseError } from '../util/missingCaseError.js'; import { drop } from '../util/drop.js'; @@ -108,7 +107,7 @@ import { badgeFromServerSchema } from '../badges/parseBadgesFromServer.js'; import { ZERO_DECIMAL_CURRENCIES } from '../util/currency.js'; import type { JobCancelReason } from '../jobs/types.js'; -const { escapeRegExp, isNumber, isString, isObject, throttle } = lodash; +const { escapeRegExp, isNumber, throttle } = lodash; const log = createLogger('WebAPI'); @@ -771,23 +770,6 @@ const RESOURCE_CALLS = { releaseNotes: 'static/release-notes', }; -type InitializeOptionsType = { - chatServiceUrl: string; - storageUrl: string; - updatesUrl: string; - resourcesUrl: string; - cdnUrlObject: { - readonly '0': string; - readonly [propName: string]: string; - }; - certificateAuthority: string; - contentProxyUrl: string; - proxyUrl: string | undefined; - version: string; - disableIPv6: boolean; - stripePublishableKey: string; -}; - export type MessageType = Readonly<{ type: number; destinationDeviceId: number; @@ -851,10 +833,6 @@ export type WebAPIConnectOptionsType = WebAPICredentials & { hasBuildExpired: boolean; }; -export type WebAPIConnectType = { - connect: (options: WebAPIConnectOptionsType) => WebAPIType; -}; - type StickerPackManifestType = Uint8Array; export type GroupCredentialType = { @@ -1543,297 +1521,6 @@ export type SubscriptionResponseType = z.infer< typeof subscriptionResponseSchema >; -export type WebAPIType = { - startRegistration(): unknown; - finishRegistration(baton: unknown): void; - cancelInflightRequests: (reason: JobCancelReason) => void; - cdsLookup: (options: CdsLookupOptionsType) => Promise; - createAccount: ( - options: CreateAccountOptionsType - ) => Promise; - confirmIntentWithStripe: ( - options: ConfirmIntentWithStripeOptionsType - ) => Promise; - createPaymentMethodWithStripe: ( - options: CreatePaymentMethodWithStripeOptionsType - ) => Promise; - createBoostPaymentIntent: ( - options: CreateBoostOptionsType - ) => Promise; - createGroup: ( - group: Proto.IGroup, - options: GroupCredentialsType - ) => Promise; - deleteUsername: (abortSignal?: AbortSignal) => Promise; - downloadOnboardingStories: ( - version: string, - imageFiles: Array - ) => Promise>; - getAttachmentFromBackupTier: ( - args: GetAttachmentFromBackupTierArgsType - ) => Promise; - getAttachment: (args: { - cdnKey: string; - cdnNumber?: number; - options?: { - disableRetries?: boolean; - timeout?: number; - downloadOffset?: number; - abortSignal?: AbortSignal; - }; - }) => Promise; - getAttachmentUploadForm: () => Promise; - getAvatar: (path: string) => Promise; - createBoostReceiptCredentials: ( - options: CreateBoostReceiptCredentialsOptionsType - ) => Promise>; - redeemReceipt: (options: RedeemReceiptOptionsType) => Promise; - getHasSubscription: (subscriberId: Uint8Array) => Promise; - getGroup: (options: GroupCredentialsType) => Promise; - getGroupFromLink: ( - inviteLinkPassword: string | undefined, - auth: GroupCredentialsType - ) => Promise; - getGroupAvatar: (key: string) => Promise; - getGroupCredentials: ( - options: GetGroupCredentialsOptionsType - ) => Promise; - getExternalGroupCredential: ( - options: GroupCredentialsType - ) => Promise; - getGroupLog: ( - options: GetGroupLogOptionsType, - credentials: GroupCredentialsType - ) => Promise; - getIceServers: () => Promise; - getKeysForServiceId: ( - serviceId: ServiceIdString, - deviceId?: number - ) => Promise; - getKeysForServiceIdUnauth: ( - serviceId: ServiceIdString, - deviceId?: number, - options?: { accessKey?: string; groupSendToken?: GroupSendToken } - ) => Promise; - getMyKeyCounts: ( - serviceIdKind: ServiceIdKind - ) => Promise>; - getOnboardingStoryManifest: () => Promise<{ - version: string; - languages: Record>; - }>; - getProfile: ( - serviceId: ServiceIdString, - options: ProfileFetchAuthRequestOptions - ) => Promise; - getAccountForUsername: ( - options: GetAccountForUsernameOptionsType - ) => Promise; - getDevices: () => Promise; - getProfileUnauth: ( - serviceId: ServiceIdString, - options: ProfileFetchUnauthRequestOptions - ) => Promise; - getBadgeImageFile: (imageUrl: string) => Promise; - getSubscriptionConfiguration: () => Promise; - getSubscription: ( - subscriberId: Uint8Array - ) => Promise; - getProvisioningResource: ( - handler: IRequestHandler, - timeout?: number - ) => Promise; - getSenderCertificate: ( - withUuid?: boolean - ) => Promise; - getReleaseNote: ( - options: GetReleaseNoteOptionsType - ) => Promise; - getReleaseNoteHash: ( - options: GetReleaseNoteOptionsType - ) => Promise; - getReleaseNotesManifest: () => Promise; - getReleaseNotesManifestHash: () => Promise; - getReleaseNoteImageAttachment: ( - path: string - ) => Promise; - getSticker: (packId: string, stickerId: number) => Promise; - getStickerPackManifest: (packId: string) => Promise; - getStorageCredentials: MessageSender['getStorageCredentials']; - getStorageManifest: MessageSender['getStorageManifest']; - getStorageRecords: MessageSender['getStorageRecords']; - fetchLinkPreviewMetadata: ( - href: string, - abortSignal: AbortSignal - ) => Promise; - fetchLinkPreviewImage: ( - href: string, - abortSignal: AbortSignal - ) => Promise; - linkDevice: (options: LinkDeviceOptionsType) => Promise; - unlink: () => Promise; - fetchJsonViaProxy: ( - params: ProxiedRequestParams - ) => Promise; - fetchBytesViaProxy: ( - params: ProxiedRequestParams - ) => Promise; - makeSfuRequest: ( - targetUrl: string, - type: HTTPCodeType, - headers: HeaderListType, - body: Uint8Array | undefined - ) => Promise; - modifyGroup: ( - changes: Proto.GroupChange.IActions, - options: GroupCredentialsType, - inviteLinkBase64?: string - ) => Promise; - modifyStorageRecords: MessageSender['modifyStorageRecords']; - postBatchIdentityCheck: ( - elements: VerifyServiceIdRequestType - ) => Promise; - putEncryptedAttachment: ( - encryptedBin: (start: number, end?: number) => Readable, - encryptedSize: number, - uploadForm: AttachmentUploadFormResponseType - ) => Promise; - putProfile: ( - jsonData: ProfileRequestDataType - ) => Promise; - putStickers: ( - encryptedManifest: Uint8Array, - encryptedStickers: ReadonlyArray, - onProgress?: () => void - ) => Promise; - reserveUsername: ( - options: ReserveUsernameOptionsType - ) => Promise; - confirmUsername( - options: ConfirmUsernameOptionsType - ): Promise; - replaceUsernameLink: ( - options: ReplaceUsernameLinkOptionsType - ) => Promise; - deleteUsernameLink: () => Promise; - resolveUsernameLink: ( - serverId: string - ) => Promise; - registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise; - registerKeys: ( - genKeys: UploadKeysType, - serviceIdKind: ServiceIdKind - ) => Promise; - reportMessage: (options: ReportMessageOptionsType) => Promise; - requestVerification: ( - number: string, - captcha: string, - transport: VerificationTransport - ) => Promise; - checkAccountExistence: (serviceId: ServiceIdString) => Promise; - sendMessages: ( - destination: ServiceIdString, - messageArray: ReadonlyArray, - timestamp: number, - options: { online?: boolean; story?: boolean; urgent?: boolean } - ) => Promise; - sendMessagesUnauth: ( - destination: ServiceIdString, - messageArray: ReadonlyArray, - timestamp: number, - options: { - accessKey: string | null; - groupSendToken: GroupSendToken | null; - online?: boolean; - story?: boolean; - urgent?: boolean; - } - ) => Promise; - sendWithSenderKey: ( - payload: Uint8Array, - accessKeys: Uint8Array | null, - groupSendToken: GroupSendToken | null, - timestamp: number, - options: { - online?: boolean; - story?: boolean; - urgent?: boolean; - } - ) => Promise; - createFetchForAttachmentUpload( - attachment: AttachmentUploadFormResponseType - ): FetchFunctionType; - getBackupInfo: ( - headers: BackupPresentationHeadersType - ) => Promise; - getBackupStream: (options: GetBackupStreamOptionsType) => Promise; - getBackupFileHeaders: ( - options: Pick< - GetBackupStreamOptionsType, - 'cdn' | 'backupDir' | 'backupName' | 'headers' - > - ) => Promise<{ 'content-length': number; 'last-modified': Date }>; - getEphemeralBackupStream: ( - options: GetEphemeralBackupStreamOptionsType - ) => Promise; - getBackupUploadForm: ( - headers: BackupPresentationHeadersType - ) => Promise; - getBackupMediaUploadForm: ( - headers: BackupPresentationHeadersType - ) => Promise; - refreshBackup: (headers: BackupPresentationHeadersType) => Promise; - getBackupCredentials: ( - options: GetBackupCredentialsOptionsType - ) => Promise; - getBackupCDNCredentials: ( - options: GetBackupCDNCredentialsOptionsType - ) => Promise; - getTransferArchive: ( - options: GetTransferArchiveOptionsType - ) => Promise; - setBackupId: (options: SetBackupIdOptionsType) => Promise; - setBackupSignatureKey: ( - options: SetBackupSignatureKeyOptionsType - ) => Promise; - backupMediaBatch: ( - options: BackupMediaBatchOptionsType - ) => Promise; - backupListMedia: ( - options: BackupListMediaOptionsType - ) => Promise; - backupDeleteMedia: (options: BackupDeleteMediaOptionsType) => Promise; - callLinkCreateAuth: ( - requestBase64: string - ) => Promise; - setPhoneNumberDiscoverability: (newValue: boolean) => Promise; - updateDeviceName: (deviceName: string) => Promise; - uploadAvatar: ( - uploadAvatarRequestHeaders: UploadAvatarHeadersType, - avatarData: Uint8Array - ) => Promise; - uploadGroupAvatar: ( - avatarData: Uint8Array, - options: GroupCredentialsType - ) => Promise; - whoami: () => Promise; - sendChallengeResponse: (challengeResponse: ChallengeType) => Promise; - getConfig: (configHash?: string) => Promise; - authenticate: (credentials: WebAPICredentials) => Promise; - logout: () => Promise; - getServerAlerts: () => Array; - getSocketStatus: () => SocketStatuses; - registerRequestHandler: (handler: IRequestHandler) => void; - unregisterRequestHandler: (handler: IRequestHandler) => void; - onHasStoriesDisabledChange: (newValue: boolean) => void; - checkSockets: () => void; - isOnline: () => boolean | undefined; - onNavigatorOnline: () => Promise; - onNavigatorOffline: () => Promise; - onExpiration: (reason: SocketExpirationReason) => Promise; - reconnect: () => Promise; -}; - export type UploadSignedPreKeyType = { keyId: number; publicKey: PublicKey; @@ -1933,3196 +1620,3028 @@ export type ProxiedRequestOptionsType = { end?: number; }; -export type TopLevelType = { - multiRecipient200ResponseSchema: typeof multiRecipient200ResponseSchema; - multiRecipient409ResponseSchema: typeof multiRecipient409ResponseSchema; - multiRecipient410ResponseSchema: typeof multiRecipient410ResponseSchema; - initialize: (options: InitializeOptionsType) => WebAPIConnectType; -}; - type InflightCallback = (cancelReason: string) => unknown; const libsignalNet = getLibsignalNet(); -// We first set up the data that won't change during this session of the app -export function initialize({ - chatServiceUrl, +const { + serverUrl: chatServiceUrl, storageUrl, updatesUrl, resourcesUrl, - cdnUrlObject, certificateAuthority, contentProxyUrl, proxyUrl, version, stripePublishableKey, -}: InitializeOptionsType): WebAPIConnectType { - if (!isString(chatServiceUrl)) { - throw new Error('WebAPI.initialize: Invalid chatServiceUrl'); - } - if (!isString(storageUrl)) { - throw new Error('WebAPI.initialize: Invalid storageUrl'); - } - if (!isString(updatesUrl)) { - throw new Error('WebAPI.initialize: Invalid updatesUrl'); - } - if (!isString(resourcesUrl)) { - throw new Error('WebAPI.initialize: Invalid updatesUrl (general)'); - } - if (!isObject(cdnUrlObject)) { - throw new Error('WebAPI.initialize: Invalid cdnUrlObject'); - } - if (!isString(cdnUrlObject['0'])) { - throw new Error('WebAPI.initialize: Missing CDN 0 configuration'); - } - if (!isString(cdnUrlObject['2'])) { - throw new Error('WebAPI.initialize: Missing CDN 2 configuration'); - } - if (!isString(cdnUrlObject['3'])) { - throw new Error('WebAPI.initialize: Missing CDN 3 configuration'); - } - if (!isString(certificateAuthority)) { - throw new Error('WebAPI.initialize: Invalid certificateAuthority'); - } - if (!isString(contentProxyUrl)) { - throw new Error('WebAPI.initialize: Invalid contentProxyUrl'); - } - if (proxyUrl && !isString(proxyUrl)) { - throw new Error('WebAPI.initialize: Invalid proxyUrl'); - } - if (!isString(version)) { - throw new Error('WebAPI.initialize: Invalid version'); - } - if (!isString(stripePublishableKey)) { - throw new Error('WebAPI.initialize: Invalid stripePublishableKey'); +} = window.SignalContext.config; + +const cdnUrlObject: Readonly<{ + '0': string; + [propName: string]: string; +}> = { + 0: window.SignalContext.config.cdnUrl0, + 2: window.SignalContext.config.cdnUrl2, + 3: window.SignalContext.config.cdnUrl3, +}; + +// We store server alerts (returned on the WS upgrade response headers) so that the app +// can query them later, which is necessary if they arrive before app state is ready +let serverAlerts: Array = []; + +let username: string | undefined; +let password: string | undefined; + +let activeRegistration: ExplodePromiseResultType | undefined; + +const PARSE_RANGE_HEADER = /\/(\d+)$/; +const PARSE_GROUP_LOG_RANGE_HEADER = + /^versions\s+(\d{1,10})-(\d{1,10})\/(\d{1,10})/; + +const libsignalRemoteConfig = new Map(); +if (isRemoteConfigEnabled('desktop.libsignalNet.enforceMinimumTls')) { + log.info('libsignal net will require TLS 1.3'); + libsignalRemoteConfig.set('enforceMinimumTls', 'true'); +} +if (isRemoteConfigEnabled('desktop.libsignalNet.shadowUnauthChatWithNoise')) { + log.info('libsignal net will shadow unauth chat connections'); + libsignalRemoteConfig.set('shadowUnauthChatWithNoise', 'true'); +} +if (isRemoteConfigEnabled('desktop.libsignalNet.shadowAuthChatWithNoise')) { + log.info('libsignal net will shadow auth chat connections'); + libsignalRemoteConfig.set('shadowAuthChatWithNoise', 'true'); +} +const perMessageDeflateConfigKey = isProduction(version) + ? 'desktop.libsignalNet.chatPermessageDeflate.prod' + : 'desktop.libsignalNet.chatPermessageDeflate'; +if (isRemoteConfigEnabled(perMessageDeflateConfigKey)) { + libsignalRemoteConfig.set('chatPermessageDeflate', 'true'); +} +libsignalNet.setRemoteConfig(libsignalRemoteConfig); + +const socketManager = new SocketManager(libsignalNet, { + url: chatServiceUrl, + certificateAuthority, + version, + proxyUrl, +}); + +socketManager.on('statusChange', () => { + window.Whisper.events.emit('socketStatusChange'); +}); + +socketManager.on('online', () => { + window.Whisper.events.emit('online'); +}); + +socketManager.on('offline', () => { + window.Whisper.events.emit('offline'); +}); + +socketManager.on('authError', () => { + window.Whisper.events.emit('unlinkAndDisconnect'); +}); + +socketManager.on('firstEnvelope', incoming => { + window.Whisper.events.emit('firstEnvelope', incoming); +}); + +socketManager.on('serverAlerts', alerts => { + log.info(`onServerAlerts: number of alerts received: ${alerts.length}`); + serverAlerts = alerts; +}); + +const cds = new CDSI(libsignalNet, { + logger: log, + proxyUrl, + + async getAuth() { + return (await _ajax({ + host: 'chatService', + call: 'directoryAuthV2', + httpType: 'GET', + responseType: 'json', + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + })) as CDSAuthType; + }, +}); + +export async function connect({ + username: initialUsername, + password: initialPassword, + hasStoriesDisabled, + hasBuildExpired, +}: WebAPIConnectOptionsType): Promise { + username = initialUsername; + password = initialPassword; + + if (hasBuildExpired) { + drop(socketManager.onExpiration('build')); } - // We store server alerts (returned on the WS upgrade response headers) so that the app - // can query them later, which is necessary if they arrive before app state is ready - let serverAlerts: Array = []; + await socketManager.onHasStoriesDisabledChange(hasStoriesDisabled); + await socketManager.authenticate({ username, password }); +} - // Thanks to function-hoisting, we can put this return statement before all of the - // below function definitions. - return { - connect, +const inflightRequests = new Set(); +function registerInflightRequest(request: InflightCallback) { + inflightRequests.add(request); +} +function unregisterInFlightRequest(request: InflightCallback) { + inflightRequests.delete(request); +} +export function cancelInflightRequests(reason: JobCancelReason): void { + const logId = `cancelInflightRequests/${reason}`; + log.warn(`${logId}: Canceling ${inflightRequests.size} requests`); + for (const request of inflightRequests) { + try { + request(reason); + } catch (error: unknown) { + log.error(`${logId}: Failed to cancel request: ${toLogFormat(error)}`); + } + } + inflightRequests.clear(); + log.warn(`${logId}: Done`); +} + +let fetchAgent: Agent | undefined; +const fetchForLinkPreviews: linkPreviewFetch.FetchFn = async (href, init) => { + if (!fetchAgent) { + if (proxyUrl) { + fetchAgent = await createProxyAgent(proxyUrl); + } else { + fetchAgent = createHTTPSAgent({ + keepAlive: false, + maxCachedSessions: 0, + }); + } + } + return fetch(href, { ...init, agent: fetchAgent }); +}; + +function _ajax(param: AjaxOptionsType<'bytes', never>): Promise; +function _ajax( + param: AjaxOptionsType<'byteswithdetails', never> +): Promise; +function _ajax( + param: AjaxOptionsType<'json', OutputShape> +): Promise; +function _ajax( + param: AjaxOptionsType<'jsonwithdetails', OutputShape> +): Promise>; + +async function _ajax( + param: AjaxOptionsType +): Promise { + const continueDuringRegistration = + param.host === 'chatService' && + (param.unauthenticated || param.isRegistration); + if (activeRegistration && !continueDuringRegistration) { + log.info('request blocked by active registration'); + const start = Date.now(); + await activeRegistration.promise; + const duration = Date.now() - start; + log.info(`request unblocked after ${duration}ms`); + } + + if (!param.urlParameters) { + param.urlParameters = ''; + } + + let host: string; + let path: string; + switch (param.host) { + case 'chatService': + [host, path] = [chatServiceUrl, CHAT_CALLS[param.call]]; + break; + case 'resources': + [host, path] = [resourcesUrl, RESOURCE_CALLS[param.call]]; + break; + case 'storageService': + [host, path] = [storageUrl, STORAGE_CALLS[param.call]]; + break; + default: + throw missingCaseError(param); + } + const useWebSocketForEndpoint = param.host === 'chatService'; + + const outerParams: PromiseAjaxOptionsType = { + socketManager: useWebSocketForEndpoint ? socketManager : undefined, + basicAuth: 'basicAuth' in param ? param.basicAuth : undefined, + certificateAuthority, + chatServiceUrl, + contentType: param.contentType || 'application/json; charset=utf-8', + data: + param.data || + (param.jsonData ? JSON.stringify(param.jsonData) : undefined), + headers: param.headers, + host, + password: 'password' in param ? param.password : password, + path: path + param.urlParameters, + proxyUrl, + responseType: param.responseType ?? ('raw' as Type), + timeout: param.timeout, + type: param.httpType, + user: 'username' in param ? param.username : username, + redactUrl: param.redactUrl, + storageUrl, + validateResponse: param.validateResponse, + version, + unauthenticated: + 'unauthenticated' in param ? param.unauthenticated : undefined, + accessKey: 'accessKey' in param ? param.accessKey : undefined, + groupSendToken: + 'groupSendToken' in param ? param.groupSendToken : undefined, + abortSignal: param.abortSignal, + zodSchema: param.zodSchema, }; - // Then we connect to the server with user-specific information. This is the only API - // exposed to the browser context, ensuring that it can't connect to arbitrary - // locations. - function connect({ - username: initialUsername, - password: initialPassword, - hasStoriesDisabled, - hasBuildExpired, - }: WebAPIConnectOptionsType) { - let username = initialUsername; - let password = initialPassword; - const PARSE_RANGE_HEADER = /\/(\d+)$/; - const PARSE_GROUP_LOG_RANGE_HEADER = - /^versions\s+(\d{1,10})-(\d{1,10})\/(\d{1,10})/; - - let activeRegistration: ExplodePromiseResultType | undefined; - - const libsignalRemoteConfig = new Map(); - if (isRemoteConfigEnabled('desktop.libsignalNet.enforceMinimumTls')) { - log.info('libsignal net will require TLS 1.3'); - libsignalRemoteConfig.set('enforceMinimumTls', 'true'); + try { + return await _outerAjax(null, outerParams); + } catch (e) { + if (!(e instanceof HTTPError)) { + throw e; } - if ( - isRemoteConfigEnabled('desktop.libsignalNet.shadowUnauthChatWithNoise') - ) { - log.info('libsignal net will shadow unauth chat connections'); - libsignalRemoteConfig.set('shadowUnauthChatWithNoise', 'true'); + const translatedError = translateError(e); + if (translatedError) { + throw translatedError; } - if (isRemoteConfigEnabled('desktop.libsignalNet.shadowAuthChatWithNoise')) { - log.info('libsignal net will shadow auth chat connections'); - libsignalRemoteConfig.set('shadowAuthChatWithNoise', 'true'); - } - const perMessageDeflateConfigKey = isProduction(version) - ? 'desktop.libsignalNet.chatPermessageDeflate.prod' - : 'desktop.libsignalNet.chatPermessageDeflate'; - if (isRemoteConfigEnabled(perMessageDeflateConfigKey)) { - libsignalRemoteConfig.set('chatPermessageDeflate', 'true'); - } - libsignalNet.setRemoteConfig(libsignalRemoteConfig); + throw e; + } +} - const socketManager = new SocketManager(libsignalNet, { - url: chatServiceUrl, - certificateAuthority, - version, - proxyUrl, - hasStoriesDisabled, - }); +function serializeSignedPreKey( + preKey?: UploadSignedPreKeyType | UploadKyberPreKeyType +): SerializedSignedPreKeyType | undefined { + if (preKey == null) { + return undefined; + } - socketManager.on('statusChange', () => { - window.Whisper.events.emit('socketStatusChange'); - }); + const { keyId, publicKey, signature } = preKey; - socketManager.on('online', () => { - window.Whisper.events.emit('online'); - }); + return { + keyId, + publicKey: Bytes.toBase64(publicKey.serialize()), + signature: Bytes.toBase64(signature), + }; +} - socketManager.on('offline', () => { - window.Whisper.events.emit('offline'); - }); +function serviceIdKindToQuery(kind: ServiceIdKind): string { + let value: string; + if (kind === ServiceIdKind.ACI) { + value = 'aci'; + } else if (kind === ServiceIdKind.PNI) { + value = 'pni'; + } else { + throw new Error(`Unsupported ServiceIdKind: ${kind}`); + } + return `identity=${value}`; +} - socketManager.on('authError', () => { - window.Whisper.events.emit('unlinkAndDisconnect'); - }); +export async function whoami(): Promise { + return _ajax({ + host: 'chatService', + call: 'whoami', + httpType: 'GET', + responseType: 'json', + zodSchema: whoamiResultZod, + }); +} - socketManager.on('firstEnvelope', incoming => { - window.Whisper.events.emit('firstEnvelope', incoming); - }); +export async function sendChallengeResponse( + challengeResponse: ChallengeType +): Promise { + await _ajax({ + host: 'chatService', + call: 'challenge', + httpType: 'PUT', + jsonData: challengeResponse, + responseType: 'bytes', + }); +} - socketManager.on('serverAlerts', alerts => { - log.info(`onServerAlerts: number of alerts received: ${alerts.length}`); - serverAlerts = alerts; - }); +export async function authenticate({ + username: newUsername, + password: newPassword, +}: WebAPICredentials): Promise { + username = newUsername; + password = newPassword; - if (hasBuildExpired) { - drop(socketManager.onExpiration('build')); - } + await socketManager.authenticate({ username, password }); +} - drop(socketManager.authenticate({ username, password })); +export async function logout(): Promise { + username = ''; + password = ''; - const cds = new CDSI(libsignalNet, { - logger: log, - proxyUrl, + await socketManager.logout(); +} - async getAuth() { - return (await _ajax({ - host: 'chatService', - call: 'directoryAuthV2', - httpType: 'GET', - responseType: 'json', - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - })) as CDSAuthType; - }, - }); +export function getSocketStatus(): SocketStatuses { + return socketManager.getStatus(); +} - const inflightRequests = new Set(); - function registerInflightRequest(request: InflightCallback) { - inflightRequests.add(request); - } - function unregisterInFlightRequest(request: InflightCallback) { - inflightRequests.delete(request); - } - function cancelInflightRequests(reason: string) { - const logId = `cancelInflightRequests/${reason}`; - log.warn(`${logId}: Canceling ${inflightRequests.size} requests`); - for (const request of inflightRequests) { - try { - request(reason); - } catch (error: unknown) { - log.error( - `${logId}: Failed to cancel request: ${toLogFormat(error)}` - ); - } - } - inflightRequests.clear(); - log.warn(`${logId}: Done`); - } +export function getServerAlerts(): Array { + return serverAlerts; +} - let fetchAgent: Agent | undefined; - const fetchForLinkPreviews: linkPreviewFetch.FetchFn = async ( - href, - init - ) => { - if (!fetchAgent) { - if (proxyUrl) { - fetchAgent = await createProxyAgent(proxyUrl); - } else { - fetchAgent = createHTTPSAgent({ - keepAlive: false, - maxCachedSessions: 0, - }); - } - } - return fetch(href, { ...init, agent: fetchAgent }); - }; +export function checkSockets(): void { + // Intentionally not awaiting + void socketManager.check(); +} - // Thanks, function hoisting! +export function isOnline(): boolean | undefined { + return socketManager.isOnline; +} + +export async function onNavigatorOnline(): Promise { + await socketManager.onNavigatorOnline(); +} + +export async function onNavigatorOffline(): Promise { + await socketManager.onNavigatorOffline(); +} + +export async function onExpiration( + reason: SocketExpirationReason +): Promise { + await socketManager.onExpiration(reason); +} + +export async function reconnect(): Promise { + await socketManager.reconnect(); +} + +export function registerRequestHandler(handler: IRequestHandler): void { + socketManager.registerRequestHandler(handler); +} + +export function unregisterRequestHandler(handler: IRequestHandler): void { + socketManager.unregisterRequestHandler(handler); +} + +export function onHasStoriesDisabledChange(newValue: boolean): void { + void socketManager.onHasStoriesDisabledChange(newValue); +} + +export async function getConfig( + configHash?: string +): Promise { + const { data, response } = await _ajax({ + host: 'chatService', + call: 'configV2', + httpType: 'GET', + responseType: 'jsonwithdetails', + zodSchema: z.union([ + remoteConfigResponseZod, + // When a 304 is returned, the body of the response is empty. + z.literal(''), + ]), + headers: { + ...(configHash && { 'if-none-match': configHash }), + }, + }); + + const serverTimestamp = safeParseNumber( + response.headers.get('x-signal-timestamp') || '' + ); + + if (serverTimestamp == null) { + throw new Error('Missing required x-signal-timestamp header'); + } + + const newConfigHash = response.headers.get('etag'); + if (newConfigHash == null) { + throw new Error('Missing required ETag header'); + } + + const partialResponse = { serverTimestamp, configHash: newConfigHash }; + + if (response.status === 304) { return { - authenticate, - backupDeleteMedia, - backupListMedia, - backupMediaBatch, - cancelInflightRequests, - cdsLookup, - checkAccountExistence, - checkSockets, - createAccount, - callLinkCreateAuth, - createFetchForAttachmentUpload, - confirmIntentWithStripe, - confirmUsername, - createBoostPaymentIntent, - createBoostReceiptCredentials, - createGroup, - createPaymentMethodWithStripe, - deleteUsername, - deleteUsernameLink, - downloadOnboardingStories, - fetchLinkPreviewImage, - fetchLinkPreviewMetadata, - finishRegistration, - getAccountForUsername, - getAttachment, - getAttachmentFromBackupTier, - getAttachmentUploadForm, - getAvatar, - getBackupCredentials, - getBackupCDNCredentials, - getBackupInfo, - getBackupStream, - getBackupFileHeaders, - getBackupMediaUploadForm, - getBackupUploadForm, - getBadgeImageFile, - getConfig, - getDevices, - getGroup, - getGroupAvatar, - getGroupCredentials, - getEphemeralBackupStream, - getExternalGroupCredential, - getGroupFromLink, - getGroupLog, - getHasSubscription, - getIceServers, - getKeysForServiceId, - getKeysForServiceIdUnauth, - getMyKeyCounts, - getOnboardingStoryManifest, - getProfile, - getProfileUnauth, - getProvisioningResource, - getReleaseNote, - getReleaseNoteHash, - getReleaseNotesManifest, - getReleaseNotesManifestHash, - getReleaseNoteImageAttachment, - getTransferArchive, - getSenderCertificate, - getServerAlerts, - getSocketStatus, - getSticker, - getStickerPackManifest, - getStorageCredentials, - getStorageManifest, - getStorageRecords, - getSubscription, - getSubscriptionConfiguration, - linkDevice, - logout, - fetchJsonViaProxy, - fetchBytesViaProxy, - makeSfuRequest, - modifyGroup, - modifyStorageRecords, - onHasStoriesDisabledChange, - isOnline, - onNavigatorOffline, - onNavigatorOnline, - onExpiration, - postBatchIdentityCheck, - putEncryptedAttachment, - putProfile, - putStickers, - reconnect, - redeemReceipt, - refreshBackup, - registerCapabilities, - registerKeys, - registerRequestHandler, - resolveUsernameLink, - replaceUsernameLink, - reportMessage, - requestVerification, - reserveUsername, - sendChallengeResponse, - sendMessages, - sendMessagesUnauth, - sendWithSenderKey, - setBackupId, - setBackupSignatureKey, - setPhoneNumberDiscoverability, - startRegistration, - unlink, - unregisterRequestHandler, - updateDeviceName, - uploadAvatar, - uploadGroupAvatar, - whoami, + config: 'unmodified', + ...partialResponse, }; + } - function _ajax(param: AjaxOptionsType<'bytes', never>): Promise; - function _ajax( - param: AjaxOptionsType<'byteswithdetails', never> - ): Promise; - function _ajax( - param: AjaxOptionsType<'json', OutputShape> - ): Promise; - function _ajax( - param: AjaxOptionsType<'jsonwithdetails', OutputShape> - ): Promise>; + if (data === '') { + throw new Error('Empty data returned for non-304'); + } - async function _ajax( - param: AjaxOptionsType - ): Promise { - const continueDuringRegistration = - param.host === 'chatService' && - (param.unauthenticated || param.isRegistration); - if (activeRegistration && !continueDuringRegistration) { - log.info('request blocked by active registration'); - const start = Date.now(); - await activeRegistration.promise; - const duration = Date.now() - start; - log.info(`request unblocked after ${duration}ms`); - } + const { config: newConfig } = data; - if (!param.urlParameters) { - param.urlParameters = ''; - } + const config = new Map( + Object.entries(newConfig).filter( + ([name, _value]) => + name.startsWith('desktop.') || + name.startsWith('global.') || + name.startsWith('cds.') + ) + ); - let host: string; - let path: string; - switch (param.host) { - case 'chatService': - [host, path] = [chatServiceUrl, CHAT_CALLS[param.call]]; - break; - case 'resources': - [host, path] = [resourcesUrl, RESOURCE_CALLS[param.call]]; - break; - case 'storageService': - [host, path] = [storageUrl, STORAGE_CALLS[param.call]]; - break; - default: - throw missingCaseError(param); - } - const useWebSocketForEndpoint = param.host === 'chatService'; + return { + config, + ...partialResponse, + }; +} - const outerParams: PromiseAjaxOptionsType = { - socketManager: useWebSocketForEndpoint ? socketManager : undefined, - basicAuth: 'basicAuth' in param ? param.basicAuth : undefined, - certificateAuthority, - chatServiceUrl, - contentType: param.contentType || 'application/json; charset=utf-8', - data: - param.data || - (param.jsonData ? JSON.stringify(param.jsonData) : undefined), - headers: param.headers, - host, - password: 'password' in param ? param.password : password, - path: path + param.urlParameters, - proxyUrl, - responseType: param.responseType ?? ('raw' as Type), - timeout: param.timeout, - type: param.httpType, - user: 'username' in param ? param.username : username, - redactUrl: param.redactUrl, - storageUrl, - validateResponse: param.validateResponse, - version, - unauthenticated: - 'unauthenticated' in param ? param.unauthenticated : undefined, - accessKey: 'accessKey' in param ? param.accessKey : undefined, - groupSendToken: - 'groupSendToken' in param ? param.groupSendToken : undefined, - abortSignal: param.abortSignal, - zodSchema: param.zodSchema, - }; +export async function getSenderCertificate( + omitE164?: boolean +): Promise { + return (await _ajax({ + host: 'chatService', + call: 'deliveryCert', + httpType: 'GET', + responseType: 'json', + validateResponse: { certificate: 'string' }, + ...(omitE164 ? { urlParameters: '?includeE164=false' } : {}), + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + })) as GetSenderCertificateResultType; +} - try { - return await _outerAjax(null, outerParams); - } catch (e) { - if (!(e instanceof HTTPError)) { - throw e; - } - const translatedError = translateError(e); - if (translatedError) { - throw translatedError; - } - throw e; - } +export async function getStorageCredentials(): Promise { + return _ajax({ + host: 'chatService', + call: 'storageToken', + httpType: 'GET', + responseType: 'json', + zodSchema: StorageServiceCredentialsSchema, + }); +} + +export async function getOnboardingStoryManifest(): Promise<{ + version: string; + languages: Record>; +}> { + const res = await _ajax({ + call: 'getOnboardingStoryManifest', + host: 'resources', + httpType: 'GET', + responseType: 'json', + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + }); + + return res as { + version: string; + languages: Record>; + }; +} + +export async function redeemReceipt( + options: RedeemReceiptOptionsType +): Promise { + await _ajax({ + host: 'chatService', + call: 'redeemReceipt', + httpType: 'POST', + jsonData: options, + responseType: 'byteswithdetails', + }); +} + +export async function getReleaseNoteHash({ + uuid, + locale, +}: { + uuid: string; + locale: string; +}): Promise { + const { response } = await _ajax({ + call: 'releaseNotes', + host: 'resources', + httpType: 'HEAD', + urlParameters: `/${uuid}/${locale}.json`, + responseType: 'byteswithdetails', + }); + + const etag = response.headers.get('etag'); + + if (etag == null) { + return undefined; + } + + return etag; +} +export async function getReleaseNote({ + uuid, + locale, +}: { + uuid: string; + locale: string; +}): Promise { + return _ajax({ + call: 'releaseNotes', + host: 'resources', + httpType: 'GET', + responseType: 'json', + urlParameters: `/${uuid}/${locale}.json`, + zodSchema: releaseNoteSchema, + }); +} + +export async function getReleaseNotesManifest(): Promise { + return _ajax({ + call: 'releaseNotesManifest', + host: 'resources', + httpType: 'GET', + responseType: 'json', + zodSchema: releaseNotesManifestSchema, + }); +} + +export async function getReleaseNotesManifestHash(): Promise< + string | undefined +> { + const { response } = await _ajax({ + call: 'releaseNotesManifest', + host: 'resources', + httpType: 'HEAD', + responseType: 'byteswithdetails', + }); + + const etag = response.headers.get('etag'); + if (etag == null) { + return undefined; + } + + return etag; +} + +export async function getReleaseNoteImageAttachment( + path: string +): Promise { + const { origin: expectedOrigin } = new URL(resourcesUrl); + const url = `${resourcesUrl}${path}`; + const { origin } = new URL(url); + strictAssert(origin === expectedOrigin, `Unexpected origin: ${origin}`); + + const { data: imageData, contentType } = await _outerAjax(url, { + certificateAuthority, + proxyUrl, + responseType: 'byteswithdetails', + timeout: 0, + type: 'GET', + version, + }); + + return { + imageData, + contentType, + }; +} + +export async function getStorageManifest( + options: StorageServiceCallOptionsType = {} +): Promise { + const { credentials, greaterThanVersion } = options; + + const { data, response } = await _ajax({ + call: 'storageManifest', + contentType: 'application/x-protobuf', + host: 'storageService', + httpType: 'GET', + responseType: 'byteswithdetails', + urlParameters: greaterThanVersion ? `/version/${greaterThanVersion}` : '', + ...credentials, + }); + + if (response.status === 204) { + throw makeHTTPError( + 'promiseAjax: error response', + response.status, + response.headers.raw(), + data, + new Error().stack + ); + } + + return data; +} + +export async function getStorageRecords( + data: Uint8Array, + options: StorageServiceCallOptionsType = {} +): Promise { + const { credentials } = options; + + return _ajax({ + call: 'storageRead', + contentType: 'application/x-protobuf', + data, + host: 'storageService', + httpType: 'PUT', + responseType: 'bytes', + ...credentials, + }); +} + +export async function modifyStorageRecords( + data: Uint8Array, + options: StorageServiceCallOptionsType = {} +): Promise { + const { credentials } = options; + + return _ajax({ + call: 'storageModify', + contentType: 'application/x-protobuf', + data, + host: 'storageService', + httpType: 'PUT', + // If we run into a conflict, the current manifest is returned - + // it will will be an Uint8Array at the response key on the Error + responseType: 'bytes', + ...credentials, + }); +} + +export async function registerCapabilities( + capabilities: CapabilitiesUploadType +): Promise { + await _ajax({ + host: 'chatService', + call: 'registerCapabilities', + httpType: 'PUT', + jsonData: capabilities, + }); +} + +export async function postBatchIdentityCheck( + elements: VerifyServiceIdRequestType +): Promise { + const res = await _ajax({ + host: 'chatService', + data: JSON.stringify({ elements }), + call: 'batchIdentityCheck', + httpType: 'POST', + responseType: 'json', + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + }); + + const result = safeParseUnknown(verifyServiceIdResponse, res); + + if (result.success) { + return result.data; + } + + log.error( + 'invalid response from postBatchIdentityCheck', + toLogFormat(result.error) + ); + + throw result.error; +} + +function getProfileUrl( + serviceId: ServiceIdString, + { + profileKeyVersion, + profileKeyCredentialRequest, + }: ProfileFetchAuthRequestOptions | ProfileFetchUnauthRequestOptions +) { + let profileUrl = `/${serviceId}`; + if (profileKeyVersion != null) { + profileUrl += `/${profileKeyVersion}`; + if (profileKeyCredentialRequest != null) { + profileUrl += + `/${profileKeyCredentialRequest}` + + '?credentialType=expiringProfileKey'; } + } else { + strictAssert( + profileKeyCredentialRequest == null, + 'getProfileUrl called without version, but with request' + ); + } - function serializeSignedPreKey( - preKey?: UploadSignedPreKeyType | UploadKyberPreKeyType - ): SerializedSignedPreKeyType | undefined { - if (preKey == null) { - return undefined; - } + return profileUrl; +} - const { keyId, publicKey, signature } = preKey; +export async function getProfile( + serviceId: ServiceIdString, + options: ProfileFetchAuthRequestOptions +): Promise { + const { profileKeyVersion, profileKeyCredentialRequest } = options; - return { - keyId, - publicKey: Bytes.toBase64(publicKey.serialize()), - signature: Bytes.toBase64(signature), - }; - } + return (await _ajax({ + host: 'chatService', + call: 'profile', + httpType: 'GET', + urlParameters: getProfileUrl(serviceId, options), + responseType: 'json', + redactUrl: _createRedactor( + serviceId, + profileKeyVersion, + profileKeyCredentialRequest + ), + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + })) as ProfileType; +} - function serviceIdKindToQuery(kind: ServiceIdKind): string { - let value: string; - if (kind === ServiceIdKind.ACI) { - value = 'aci'; - } else if (kind === ServiceIdKind.PNI) { - value = 'pni'; - } else { - throw new Error(`Unsupported ServiceIdKind: ${kind}`); - } - return `identity=${value}`; - } +export async function getTransferArchive({ + timeout = HOUR, + abortSignal, +}: GetTransferArchiveOptionsType): Promise { + const timeoutTime = Date.now() + timeout; - async function whoami(): Promise { - return _ajax({ - host: 'chatService', - call: 'whoami', - httpType: 'GET', - responseType: 'json', - zodSchema: whoamiResultZod, - }); - } + let remainingTime: number; + do { + remainingTime = Math.max(timeoutTime - Date.now(), 0); - async function sendChallengeResponse(challengeResponse: ChallengeType) { - await _ajax({ - host: 'chatService', - call: 'challenge', - httpType: 'PUT', - jsonData: challengeResponse, - responseType: 'bytes', - }); - } + const requestTimeoutInSecs = Math.round( + Math.min(remainingTime, 5 * MINUTE) / SECOND + ); - async function authenticate({ - username: newUsername, - password: newPassword, - }: WebAPICredentials) { - username = newUsername; - password = newPassword; + const urlParameters = timeout + ? `?timeout=${encodeURIComponent(requestTimeoutInSecs)}` + : undefined; - await socketManager.authenticate({ username, password }); - } - - async function logout() { - username = ''; - password = ''; - - await socketManager.logout(); - } - - function getSocketStatus(): SocketStatuses { - return socketManager.getStatus(); - } - - function getServerAlerts(): Array { - return serverAlerts; - } - - function checkSockets(): void { - // Intentionally not awaiting - void socketManager.check(); - } - - function isOnline(): boolean | undefined { - return socketManager.isOnline; - } - - async function onNavigatorOnline(): Promise { - await socketManager.onNavigatorOnline(); - } - - async function onNavigatorOffline(): Promise { - await socketManager.onNavigatorOffline(); - } - - async function onExpiration(reason: SocketExpirationReason): Promise { - await socketManager.onExpiration(reason); - } - - async function reconnect(): Promise { - await socketManager.reconnect(); - } - - function registerRequestHandler(handler: IRequestHandler): void { - socketManager.registerRequestHandler(handler); - } - - function unregisterRequestHandler(handler: IRequestHandler): void { - socketManager.unregisterRequestHandler(handler); - } - - function onHasStoriesDisabledChange(newValue: boolean): void { - void socketManager.onHasStoriesDisabledChange(newValue); - } - - async function getConfig( - configHash?: string - ): Promise { - const { data, response } = await _ajax({ - host: 'chatService', - call: 'configV2', - httpType: 'GET', - responseType: 'jsonwithdetails', - zodSchema: z.union([ - remoteConfigResponseZod, - // When a 304 is returned, the body of the response is empty. - z.literal(''), - ]), - headers: { - ...(configHash && { 'if-none-match': configHash }), - }, - }); - - const serverTimestamp = safeParseNumber( - response.headers.get('x-signal-timestamp') || '' - ); - - if (serverTimestamp == null) { - throw new Error('Missing required x-signal-timestamp header'); - } - - const newConfigHash = response.headers.get('etag'); - if (newConfigHash == null) { - throw new Error('Missing required ETag header'); - } - - const partialResponse = { serverTimestamp, configHash: newConfigHash }; - - if (response.status === 304) { - return { - config: 'unmodified', - ...partialResponse, - }; - } - - if (data === '') { - throw new Error('Empty data returned for non-304'); - } - - const { config: newConfig } = data; - - const config = new Map( - Object.entries(newConfig).filter( - ([name, _value]) => - name.startsWith('desktop.') || - name.startsWith('global.') || - name.startsWith('cds.') - ) - ); - - return { - config, - ...partialResponse, - }; - } - - async function getSenderCertificate(omitE164?: boolean) { - return (await _ajax({ - host: 'chatService', - call: 'deliveryCert', - httpType: 'GET', - responseType: 'json', - validateResponse: { certificate: 'string' }, - ...(omitE164 ? { urlParameters: '?includeE164=false' } : {}), - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - })) as GetSenderCertificateResultType; - } - - async function getStorageCredentials(): Promise { - return _ajax({ - host: 'chatService', - call: 'storageToken', - httpType: 'GET', - responseType: 'json', - zodSchema: StorageServiceCredentialsSchema, - }); - } - - async function getOnboardingStoryManifest() { - const res = await _ajax({ - call: 'getOnboardingStoryManifest', - host: 'resources', - httpType: 'GET', - responseType: 'json', - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - }); - - return res as { - version: string; - languages: Record>; - }; - } - - async function redeemReceipt( - options: RedeemReceiptOptionsType - ): Promise { - await _ajax({ - host: 'chatService', - call: 'redeemReceipt', - httpType: 'POST', - jsonData: options, - responseType: 'byteswithdetails', - }); - } - - async function getReleaseNoteHash({ - uuid, - locale, - }: { - uuid: string; - locale: string; - }): Promise { - const { response } = await _ajax({ - call: 'releaseNotes', - host: 'resources', - httpType: 'HEAD', - urlParameters: `/${uuid}/${locale}.json`, - responseType: 'byteswithdetails', - }); - - const etag = response.headers.get('etag'); - - if (etag == null) { - return undefined; - } - - return etag; - } - async function getReleaseNote({ - uuid, - locale, - }: { - uuid: string; - locale: string; - }): Promise { - return _ajax({ - call: 'releaseNotes', - host: 'resources', - httpType: 'GET', - responseType: 'json', - urlParameters: `/${uuid}/${locale}.json`, - zodSchema: releaseNoteSchema, - }); - } - - async function getReleaseNotesManifest(): Promise { - return _ajax({ - call: 'releaseNotesManifest', - host: 'resources', - httpType: 'GET', - responseType: 'json', - zodSchema: releaseNotesManifestSchema, - }); - } - - async function getReleaseNotesManifestHash(): Promise { - const { response } = await _ajax({ - call: 'releaseNotesManifest', - host: 'resources', - httpType: 'HEAD', - responseType: 'byteswithdetails', - }); - - const etag = response.headers.get('etag'); - if (etag == null) { - return undefined; - } - - return etag; - } - - async function getReleaseNoteImageAttachment( - path: string - ): Promise { - const { origin: expectedOrigin } = new URL(resourcesUrl); - const url = `${resourcesUrl}${path}`; - const { origin } = new URL(url); - strictAssert(origin === expectedOrigin, `Unexpected origin: ${origin}`); - - const { data: imageData, contentType } = await _outerAjax(url, { - certificateAuthority, - proxyUrl, - responseType: 'byteswithdetails', - timeout: 0, - type: 'GET', - version, - }); - - return { - imageData, - contentType, - }; - } - - async function getStorageManifest( - options: StorageServiceCallOptionsType = {} - ): Promise { - const { credentials, greaterThanVersion } = options; - - const { data, response } = await _ajax({ - call: 'storageManifest', - contentType: 'application/x-protobuf', - host: 'storageService', - httpType: 'GET', - responseType: 'byteswithdetails', - urlParameters: greaterThanVersion - ? `/version/${greaterThanVersion}` - : '', - ...credentials, - }); - - if (response.status === 204) { - throw makeHTTPError( - 'promiseAjax: error response', - response.status, - response.headers.raw(), - data, - new Error().stack - ); - } + // eslint-disable-next-line no-await-in-loop + const { data, response } = await _ajax({ + host: 'chatService', + call: 'transferArchive', + httpType: 'GET', + responseType: 'jsonwithdetails', + urlParameters, + // Add a bit of leeway to let server respond properly + timeout: (requestTimeoutInSecs + 15) * SECOND, + abortSignal, + // We may also get a 204 with no content, indicating we should try again + zodSchema: TransferArchiveSchema.or(z.literal('')), + }); + if (response.status === 200) { + strictAssert(data !== '', '200 must have data'); return data; } - async function getStorageRecords( - data: Uint8Array, - options: StorageServiceCallOptionsType = {} - ): Promise { - const { credentials } = options; + strictAssert( + response.status === 204, + 'Invalid transfer archive status code' + ); - return _ajax({ - call: 'storageRead', - contentType: 'application/x-protobuf', - data, - host: 'storageService', - httpType: 'PUT', - responseType: 'bytes', - ...credentials, - }); + if (abortSignal?.aborted) { + break; } - async function modifyStorageRecords( - data: Uint8Array, - options: StorageServiceCallOptionsType = {} - ): Promise { - const { credentials } = options; + // Timed out, see if we can retry + } while (!timeout || remainingTime != null); - return _ajax({ - call: 'storageModify', - contentType: 'application/x-protobuf', - data, - host: 'storageService', - httpType: 'PUT', - // If we run into a conflict, the current manifest is returned - - // it will will be an Uint8Array at the response key on the Error - responseType: 'bytes', - ...credentials, - }); - } + throw new Error('Timed out'); +} - async function registerCapabilities(capabilities: CapabilitiesUploadType) { - await _ajax({ - host: 'chatService', - call: 'registerCapabilities', - httpType: 'PUT', - jsonData: capabilities, - }); - } +export async function getAccountForUsername({ + hash, +}: GetAccountForUsernameOptionsType): Promise { + const hashBase64 = toWebSafeBase64(Bytes.toBase64(hash)); + return _ajax({ + host: 'chatService', + call: 'username', + httpType: 'GET', + urlParameters: `/${hashBase64}`, + responseType: 'json', + redactUrl: _createRedactor(hashBase64), + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + zodSchema: getAccountForUsernameResultZod, + }); +} - async function postBatchIdentityCheck( - elements: VerifyServiceIdRequestType - ) { - const res = await _ajax({ - host: 'chatService', - data: JSON.stringify({ elements }), - call: 'batchIdentityCheck', - httpType: 'POST', - responseType: 'json', - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - }); +export async function putProfile( + jsonData: ProfileRequestDataType +): Promise { + return _ajax({ + host: 'chatService', + call: 'profile', + httpType: 'PUT', + responseType: 'json', + jsonData, + zodSchema: uploadAvatarOrOther, + }); +} - const result = safeParseUnknown(verifyServiceIdResponse, res); +export async function getProfileUnauth( + serviceId: ServiceIdString, + options: ProfileFetchUnauthRequestOptions +): Promise { + const { + accessKey, + groupSendToken, + profileKeyVersion, + profileKeyCredentialRequest, + } = options; - if (result.success) { - return result.data; + if (profileKeyVersion != null || profileKeyCredentialRequest != null) { + // Without an up-to-date profile key, we won't be able to read the + // profile anyways so there's no point in falling back to endorsements. + strictAssert( + groupSendToken == null, + 'Should not use endorsements for fetching a versioned profile' + ); + } + + return (await _ajax({ + host: 'chatService', + call: 'profile', + httpType: 'GET', + urlParameters: getProfileUrl(serviceId, options), + responseType: 'json', + unauthenticated: true, + accessKey: accessKey ?? undefined, + groupSendToken: groupSendToken ?? undefined, + redactUrl: _createRedactor( + serviceId, + profileKeyVersion, + profileKeyCredentialRequest + ), + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + })) as ProfileType; +} + +export async function getBadgeImageFile( + imageFileUrl: string +): Promise { + strictAssert( + isBadgeImageFileUrlValid(imageFileUrl, updatesUrl), + 'getBadgeImageFile got an invalid URL. Was bad data saved?' + ); + + return _outerAjax(imageFileUrl, { + certificateAuthority, + contentType: 'application/octet-stream', + proxyUrl, + responseType: 'bytes', + timeout: 0, + type: 'GET', + redactUrl: (href: string) => { + const parsedUrl = maybeParseUrl(href); + if (!parsedUrl) { + return href; } + const { pathname } = parsedUrl; + const pattern = RegExp(escapeRegExp(pathname), 'g'); + return href.replace(pattern, `[REDACTED]${pathname.slice(-3)}`); + }, + version, + }); +} - log.error( - 'invalid response from postBatchIdentityCheck', - toLogFormat(result.error) - ); - - throw result.error; - } - - function getProfileUrl( - serviceId: ServiceIdString, - { - profileKeyVersion, - profileKeyCredentialRequest, - }: ProfileFetchAuthRequestOptions | ProfileFetchUnauthRequestOptions - ) { - let profileUrl = `/${serviceId}`; - if (profileKeyVersion != null) { - profileUrl += `/${profileKeyVersion}`; - if (profileKeyCredentialRequest != null) { - profileUrl += - `/${profileKeyCredentialRequest}` + - '?credentialType=expiringProfileKey'; - } - } else { - strictAssert( - profileKeyCredentialRequest == null, - 'getProfileUrl called without version, but with request' - ); - } - - return profileUrl; - } - - async function getProfile( - serviceId: ServiceIdString, - options: ProfileFetchAuthRequestOptions - ) { - const { profileKeyVersion, profileKeyCredentialRequest } = options; - - return (await _ajax({ - host: 'chatService', - call: 'profile', - httpType: 'GET', - urlParameters: getProfileUrl(serviceId, options), - responseType: 'json', - redactUrl: _createRedactor( - serviceId, - profileKeyVersion, - profileKeyCredentialRequest - ), - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - })) as ProfileType; - } - - async function getTransferArchive({ - timeout = HOUR, - abortSignal, - }: GetTransferArchiveOptionsType): Promise { - const timeoutTime = Date.now() + timeout; - - let remainingTime: number; - do { - remainingTime = Math.max(timeoutTime - Date.now(), 0); - - const requestTimeoutInSecs = Math.round( - Math.min(remainingTime, 5 * MINUTE) / SECOND - ); - - const urlParameters = timeout - ? `?timeout=${encodeURIComponent(requestTimeoutInSecs)}` - : undefined; - - // eslint-disable-next-line no-await-in-loop - const { data, response } = await _ajax({ - host: 'chatService', - call: 'transferArchive', - httpType: 'GET', - responseType: 'jsonwithdetails', - urlParameters, - // Add a bit of leeway to let server respond properly - timeout: (requestTimeoutInSecs + 15) * SECOND, - abortSignal, - // We may also get a 204 with no content, indicating we should try again - zodSchema: TransferArchiveSchema.or(z.literal('')), - }); - - if (response.status === 200) { - strictAssert(data !== '', '200 must have data'); - return data; - } - - strictAssert( - response.status === 204, - 'Invalid transfer archive status code' - ); - - if (abortSignal?.aborted) { - break; - } - - // Timed out, see if we can retry - } while (!timeout || remainingTime != null); - - throw new Error('Timed out'); - } - - async function getAccountForUsername({ - hash, - }: GetAccountForUsernameOptionsType) { - const hashBase64 = toWebSafeBase64(Bytes.toBase64(hash)); - return _ajax({ - host: 'chatService', - call: 'username', - httpType: 'GET', - urlParameters: `/${hashBase64}`, - responseType: 'json', - redactUrl: _createRedactor(hashBase64), - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - zodSchema: getAccountForUsernameResultZod, - }); - } - - async function putProfile( - jsonData: ProfileRequestDataType - ): Promise { - return _ajax({ - host: 'chatService', - call: 'profile', - httpType: 'PUT', - responseType: 'json', - jsonData, - zodSchema: uploadAvatarOrOther, - }); - } - - async function getProfileUnauth( - serviceId: ServiceIdString, - options: ProfileFetchUnauthRequestOptions - ) { - const { - accessKey, - groupSendToken, - profileKeyVersion, - profileKeyCredentialRequest, - } = options; - - if (profileKeyVersion != null || profileKeyCredentialRequest != null) { - // Without an up-to-date profile key, we won't be able to read the - // profile anyways so there's no point in falling back to endorsements. - strictAssert( - groupSendToken == null, - 'Should not use endorsements for fetching a versioned profile' - ); - } - - return (await _ajax({ - host: 'chatService', - call: 'profile', - httpType: 'GET', - urlParameters: getProfileUrl(serviceId, options), - responseType: 'json', - unauthenticated: true, - accessKey: accessKey ?? undefined, - groupSendToken: groupSendToken ?? undefined, - redactUrl: _createRedactor( - serviceId, - profileKeyVersion, - profileKeyCredentialRequest - ), - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - })) as ProfileType; - } - - async function getBadgeImageFile( - imageFileUrl: string - ): Promise { - strictAssert( - isBadgeImageFileUrlValid(imageFileUrl, updatesUrl), - 'getBadgeImageFile got an invalid URL. Was bad data saved?' - ); - - return _outerAjax(imageFileUrl, { - certificateAuthority, - contentType: 'application/octet-stream', - proxyUrl, - responseType: 'bytes', - timeout: 0, - type: 'GET', - redactUrl: (href: string) => { - const parsedUrl = maybeParseUrl(href); - if (!parsedUrl) { - return href; - } - const { pathname } = parsedUrl; - const pattern = RegExp(escapeRegExp(pathname), 'g'); - return href.replace(pattern, `[REDACTED]${pathname.slice(-3)}`); - }, - version, - }); - } - - async function downloadOnboardingStories( - manifestVersion: string, - imageFiles: Array - ): Promise> { - return Promise.all( - imageFiles.map(fileName => - _outerAjax( - `${resourcesUrl}/static/desktop/stories/onboarding/${manifestVersion}/${fileName}.jpg`, - { - certificateAuthority, - contentType: 'application/octet-stream', - proxyUrl, - responseType: 'bytes', - timeout: 0, - type: 'GET', - version, - } - ) - ) - ); - } - - async function getSubscriptionConfiguration(): Promise { - return _ajax({ - host: 'chatService', - call: 'subscriptionConfiguration', - httpType: 'GET', - responseType: 'json', - zodSchema: subscriptionConfigurationResultZod, - }); - } - - async function getAvatar(path: string) { - // Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our - // attachment CDN, it uses our self-signed certificate, so we pass it in. - return _outerAjax(`${cdnUrlObject['0']}/${path}`, { - certificateAuthority, - contentType: 'application/octet-stream', - proxyUrl, - responseType: 'bytes', - timeout: 90 * SECOND, - type: 'GET', - redactUrl: (href: string) => { - const pattern = RegExp(escapeRegExp(path), 'g'); - return href.replace(pattern, `[REDACTED]${path.slice(-3)}`); - }, - version, - }); - } - - async function deleteUsername(abortSignal?: AbortSignal) { - await _ajax({ - host: 'chatService', - call: 'username', - httpType: 'DELETE', - abortSignal, - }); - } - - async function reserveUsername({ - hashes, - abortSignal, - }: ReserveUsernameOptionsType) { - return _ajax({ - host: 'chatService', - call: 'reserveUsername', - httpType: 'PUT', - jsonData: { - usernameHashes: hashes.map(hash => - toWebSafeBase64(Bytes.toBase64(hash)) - ), - }, - responseType: 'json', - abortSignal, - zodSchema: reserveUsernameResultZod, - }); - } - async function confirmUsername({ - hash, - proof, - encryptedUsername, - abortSignal, - }: ConfirmUsernameOptionsType): Promise { - return _ajax({ - host: 'chatService', - call: 'confirmUsername', - httpType: 'PUT', - jsonData: { - usernameHash: toWebSafeBase64(Bytes.toBase64(hash)), - zkProof: toWebSafeBase64(Bytes.toBase64(proof)), - encryptedUsername: toWebSafeBase64(Bytes.toBase64(encryptedUsername)), - }, - responseType: 'json', - abortSignal, - zodSchema: confirmUsernameResultZod, - }); - } - - async function replaceUsernameLink({ - encryptedUsername, - keepLinkHandle, - }: ReplaceUsernameLinkOptionsType): Promise { - return _ajax({ - host: 'chatService', - call: 'usernameLink', - httpType: 'PUT', - responseType: 'json', - jsonData: { - usernameLinkEncryptedValue: toWebSafeBase64( - Bytes.toBase64(encryptedUsername) - ), - keepLinkHandle, - }, - zodSchema: replaceUsernameLinkResultZod, - }); - } - - async function deleteUsernameLink(): Promise { - await _ajax({ - host: 'chatService', - call: 'usernameLink', - httpType: 'DELETE', - }); - } - - async function resolveUsernameLink( - serverId: string - ): Promise { - return _ajax({ - host: 'chatService', - httpType: 'GET', - call: 'usernameLink', - urlParameters: `/${encodeURIComponent(serverId)}`, - responseType: 'json', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - zodSchema: resolveUsernameLinkResultZod, - }); - } - - async function reportMessage({ - senderAci, - serverGuid, - token, - }: ReportMessageOptionsType): Promise { - const jsonData = { token }; - - await _ajax({ - host: 'chatService', - call: 'reportMessage', - httpType: 'POST', - urlParameters: urlPathFromComponents([senderAci, serverGuid]), - responseType: 'bytes', - jsonData, - }); - } - - async function requestVerification( - number: string, - captcha: string, - transport: VerificationTransport - ) { - // Create a new blank session using just a E164 - const session = await libsignalNet.createRegistrationSession({ - e164: number, - }); - - // Submit a captcha solution to the session - await session.submitCaptcha(captcha); - - // Verify that captcha was accepted - if (!session.sessionState.allowedToRequestCode) { - throw new Error('requestVerification: Not allowed to send code'); - } - - // Request an SMS or Voice confirmation - await session.requestVerification({ - transport: transport === VerificationTransport.SMS ? 'sms' : 'voice', - client: 'ios', - languages: [], - }); - - // Return sessionId to be used in `createAccount` - return { sessionId: session.sessionId }; - } - - async function checkAccountExistence(serviceId: ServiceIdString) { - try { - await _ajax({ - host: 'chatService', - httpType: 'HEAD', - call: 'accountExistence', - urlParameters: `/${serviceId}`, - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - }); - return true; - } catch (error) { - if (error instanceof HTTPError && error.code === 404) { - return false; - } - - throw error; - } - } - - function startRegistration() { - strictAssert( - activeRegistration === undefined, - 'Registration already in progress' - ); - - activeRegistration = explodePromise(); - log.info('starting registration'); - - return activeRegistration; - } - - function finishRegistration(registration: unknown) { - strictAssert(activeRegistration !== undefined, 'No active registration'); - strictAssert( - activeRegistration === registration, - 'Invalid registration baton' - ); - - log.info('finishing registration'); - const current = activeRegistration; - activeRegistration = undefined; - current.resolve(); - } - - async function _withNewCredentials< - Result extends { uuid: AciString; deviceId?: number }, - >( - { username: newUsername, password: newPassword }: WebAPICredentials, - callback: () => Promise - ): Promise { - // Reset old websocket credentials and disconnect. - // AccountManager is our only caller and it will trigger - // `registration_done` which will update credentials. - await logout(); - - // Update REST credentials, though. We need them for the call below - username = newUsername; - password = newPassword; - - const result = await callback(); - - const { uuid: aci = newUsername, deviceId = 1 } = result; - - // Set final REST credentials to let `registerKeys` succeed. - username = `${aci}.${deviceId}`; - password = newPassword; - - return result; - } - - async function createAccount({ - sessionId, - number, - code, - newPassword, - registrationId, - pniRegistrationId, - accessKey, - aciPublicKey, - pniPublicKey, - aciSignedPreKey, - pniSignedPreKey, - aciPqLastResortPreKey, - pniPqLastResortPreKey, - }: CreateAccountOptionsType) { - const session = await libsignalNet.resumeRegistrationSession({ - sessionId, - e164: number, - }); - const verified = await session.verifySession(code); - - if (!verified) { - throw new Error('createAccount: invalid code'); - } - - const capabilities: CapabilitiesUploadType = { - attachmentBackfill: true, - spqr: true, - }; - - // Desktop doesn't support recovery but we need to provide a recovery password. - // Since the value isn't used, just use a random one and then throw it away. - const recoveryPassword = getRandomBytes(32); - const accountAttributes = new AccountAttributes({ - aciRegistrationId: registrationId, - pniRegistrationId, - capabilities: new Set( - Object.entries(capabilities).flatMap(([k, v]) => (v ? [k] : [])) - ), - unidentifiedAccessKey: accessKey, - unrestrictedUnidentifiedAccess: false, - recoveryPassword, - registrationLock: null, - discoverableByPhoneNumber: false, - }); - - // Massages UploadSignedPreKey into SignedPublicPreKey and likewise for Kyber. - function asSignedKey(key: { - keyId: number; - publicKey: K; - signature: Uint8Array; - }): { - id: () => number; - publicKey: () => K; - signature: () => Uint8Array; - } { - return { - id: () => key.keyId, - signature: () => key.signature, - publicKey: () => key.publicKey, - }; - } - - const { aci, pni } = await session.registerAccount({ - accountPassword: newPassword, - accountAttributes, - skipDeviceTransfer: true, - aciPublicKey, - pniPublicKey, - aciSignedPreKey: asSignedKey(aciSignedPreKey), - pniSignedPreKey: asSignedKey(pniSignedPreKey), - aciPqLastResortPreKey: asSignedKey(aciPqLastResortPreKey), - pniPqLastResortPreKey: asSignedKey(pniPqLastResortPreKey), - }); - - return { aci, pni }; - } - - async function linkDevice({ - number, - verificationCode, - encryptedDeviceName, - newPassword, - registrationId, - pniRegistrationId, - aciSignedPreKey, - pniSignedPreKey, - aciPqLastResortPreKey, - pniPqLastResortPreKey, - }: LinkDeviceOptionsType) { - const capabilities: CapabilitiesUploadType = { - attachmentBackfill: true, - spqr: true, - }; - - const jsonData = { - verificationCode, - accountAttributes: { - fetchesMessages: true, - name: encryptedDeviceName, - registrationId, - pniRegistrationId, - capabilities, - }, - aciSignedPreKey: serializeSignedPreKey(aciSignedPreKey), - pniSignedPreKey: serializeSignedPreKey(pniSignedPreKey), - aciPqLastResortPreKey: serializeSignedPreKey(aciPqLastResortPreKey), - pniPqLastResortPreKey: serializeSignedPreKey(pniPqLastResortPreKey), - }; - return _withNewCredentials( +export async function downloadOnboardingStories( + manifestVersion: string, + imageFiles: Array +): Promise> { + return Promise.all( + imageFiles.map(fileName => + _outerAjax( + `${resourcesUrl}/static/desktop/stories/onboarding/${manifestVersion}/${fileName}.jpg`, { - username: number, - password: newPassword, - }, - async () => { - const response = await _ajax({ - host: 'chatService', - isRegistration: true, - call: 'linkDevice', - httpType: 'PUT', - responseType: 'json', - jsonData, - zodSchema: linkDeviceResultZod, - }); - - return response; - } - ); - } - - async function unlink() { - if (!username) { - return; - } - - const [, deviceId] = username.split('.'); - await _ajax({ - host: 'chatService', - call: 'devices', - httpType: 'DELETE', - urlParameters: `/${deviceId}`, - }); - } - - async function getDevices() { - return _ajax({ - host: 'chatService', - call: 'devices', - httpType: 'GET', - responseType: 'json', - zodSchema: getDevicesResultZod, - }); - } - - async function updateDeviceName(deviceName: string) { - await _ajax({ - host: 'chatService', - call: 'updateDeviceName', - httpType: 'PUT', - jsonData: { - deviceName, - }, - }); - } - - async function getIceServers() { - return (await _ajax({ - host: 'chatService', - call: 'getIceServers', - httpType: 'GET', - responseType: 'json', - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - })) as GetIceServersResultType; - } - - type JSONSignedPreKeyType = { - keyId: number; - publicKey: string; - signature: string; - }; - type JSONPreKeyType = { - keyId: number; - publicKey: string; - }; - type JSONKyberPreKeyType = { - keyId: number; - publicKey: string; - signature: string; - }; - - type JSONKeysType = { - preKeys?: Array; - pqPreKeys?: Array; - pqLastResortPreKey?: JSONKyberPreKeyType; - signedPreKey?: JSONSignedPreKeyType; - }; - - async function registerKeys( - genKeys: UploadKeysType, - serviceIdKind: ServiceIdKind - ) { - const preKeys = genKeys.preKeys?.map(key => ({ - keyId: key.keyId, - publicKey: Bytes.toBase64(key.publicKey.serialize()), - })); - const pqPreKeys = genKeys.pqPreKeys?.map(key => ({ - keyId: key.keyId, - publicKey: Bytes.toBase64(key.publicKey.serialize()), - signature: Bytes.toBase64(key.signature), - })); - - if ( - !preKeys?.length && - !pqPreKeys?.length && - !genKeys.pqLastResortPreKey && - !genKeys.signedPreKey - ) { - throw new Error( - 'registerKeys: None of the four potential key types were provided!' - ); - } - if (preKeys && preKeys.length === 0) { - throw new Error('registerKeys: Attempting to upload zero preKeys!'); - } - if (pqPreKeys && pqPreKeys.length === 0) { - throw new Error('registerKeys: Attempting to upload zero pqPreKeys!'); - } - - const keys: JSONKeysType = { - preKeys, - pqPreKeys, - pqLastResortPreKey: serializeSignedPreKey(genKeys.pqLastResortPreKey), - signedPreKey: serializeSignedPreKey(genKeys.signedPreKey), - }; - - await _ajax({ - host: 'chatService', - isRegistration: true, - call: 'keys', - urlParameters: `?${serviceIdKindToQuery(serviceIdKind)}`, - httpType: 'PUT', - jsonData: keys, - }); - } - - async function getBackupInfo(headers: BackupPresentationHeadersType) { - return _ajax({ - host: 'chatService', - call: 'backup', - httpType: 'GET', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - headers, - responseType: 'json', - zodSchema: getBackupInfoResponseSchema, - }); - } - - async function getBackupStream({ - headers, - cdn, - backupDir, - backupName, - downloadOffset, - onProgress, - abortSignal, - }: GetBackupStreamOptionsType): Promise { - return _getAttachment({ - cdnPath: `/backups/${encodeURIComponent(backupDir)}/${encodeURIComponent(backupName)}`, - cdnNumber: cdn, - redactor: _createRedactor(backupDir, backupName), - headers, - options: { - downloadOffset, - onProgress, - abortSignal, - }, - }); - } - async function getBackupFileHeaders({ - headers, - cdn, - backupDir, - backupName, - }: Pick< - GetBackupStreamOptionsType, - 'headers' | 'cdn' | 'backupDir' | 'backupName' - >): Promise { - const result = await _getAttachmentHeaders({ - cdnPath: `/backups/${encodeURIComponent(backupDir)}/${encodeURIComponent(backupName)}`, - cdnNumber: cdn, - redactor: _createRedactor(backupDir, backupName), - headers, - }); - const responseHeaders = Object.fromEntries(result.entries()); - - return parseUnknown(backupFileHeadersSchema, responseHeaders as unknown); - } - - async function getEphemeralBackupStream({ - cdn, - key, - downloadOffset, - onProgress, - abortSignal, - }: GetEphemeralBackupStreamOptionsType): Promise { - return _getAttachment({ - cdnNumber: cdn, - cdnPath: `/attachments/${encodeURIComponent(key)}`, - redactor: _createRedactor(key), - options: { - downloadOffset, - onProgress, - abortSignal, - }, - }); - } - - async function getBackupMediaUploadForm( - headers: BackupPresentationHeadersType - ): Promise { - return _ajax({ - host: 'chatService', - call: 'getBackupMediaUploadForm', - httpType: 'GET', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - headers, - responseType: 'json', - zodSchema: attachmentUploadFormResponse, - }); - } - - function createFetchForAttachmentUpload({ - signedUploadLocation, - headers: uploadHeaders, - cdn, - }: AttachmentUploadFormResponseType): FetchFunctionType { - strictAssert(cdn === 3, 'Fetch can only be created for CDN 3'); - const { origin: expectedOrigin } = new URL(signedUploadLocation); - - return async ( - endpoint: string | URL, - init: RequestInit - ): Promise => { - const { origin } = new URL(endpoint); - strictAssert(origin === expectedOrigin, `Unexpected origin: ${origin}`); - - const fetchOptions = await getFetchOptions({ - // Will be overriden - type: 'GET', - certificateAuthority, + contentType: 'application/octet-stream', proxyUrl, + responseType: 'bytes', timeout: 0, + type: 'GET', version, - - headers: uploadHeaders, - }); - - return fetch(endpoint, { - ...fetchOptions, - ...init, - headers: { - ...fetchOptions.headers, - ...init.headers, - }, - }); - }; - } - - async function getBackupUploadForm( - headers: BackupPresentationHeadersType - ): Promise { - return _ajax({ - host: 'chatService', - call: 'getBackupUploadForm', - httpType: 'GET', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - headers, - responseType: 'json', - zodSchema: attachmentUploadFormResponse, - }); - } - - async function refreshBackup(headers: BackupPresentationHeadersType) { - await _ajax({ - host: 'chatService', - call: 'backup', - httpType: 'POST', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - headers, - }); - } - - async function getBackupCredentials({ - startDayInMs, - endDayInMs, - }: GetBackupCredentialsOptionsType) { - const startDayInSeconds = startDayInMs / SECOND; - const endDayInSeconds = endDayInMs / SECOND; - return _ajax({ - host: 'chatService', - call: 'getBackupCredentials', - httpType: 'GET', - urlParameters: - `?redemptionStartSeconds=${startDayInSeconds}&` + - `redemptionEndSeconds=${endDayInSeconds}`, - responseType: 'json', - zodSchema: getBackupCredentialsResponseSchema, - }); - } - - async function getBackupCDNCredentials({ - headers, - cdnNumber, - }: GetBackupCDNCredentialsOptionsType) { - return _ajax({ - host: 'chatService', - call: 'getBackupCDNCredentials', - httpType: 'GET', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - headers, - urlParameters: `?cdn=${cdnNumber}`, - responseType: 'json', - zodSchema: getBackupCDNCredentialsResponseSchema, - }); - } - - async function setBackupId({ - messagesBackupAuthCredentialRequest, - mediaBackupAuthCredentialRequest, - }: SetBackupIdOptionsType) { - await _ajax({ - host: 'chatService', - call: 'setBackupId', - httpType: 'PUT', - jsonData: { - messagesBackupAuthCredentialRequest: Bytes.toBase64( - messagesBackupAuthCredentialRequest - ), - mediaBackupAuthCredentialRequest: Bytes.toBase64( - mediaBackupAuthCredentialRequest - ), - }, - }); - } - - async function setBackupSignatureKey({ - headers, - backupIdPublicKey, - }: SetBackupSignatureKeyOptionsType) { - await _ajax({ - host: 'chatService', - call: 'setBackupSignatureKey', - httpType: 'PUT', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - headers, - jsonData: { - backupIdPublicKey: Bytes.toBase64(backupIdPublicKey), - }, - }); - } - - async function backupMediaBatch({ - headers, - items, - }: BackupMediaBatchOptionsType) { - return _ajax({ - host: 'chatService', - call: 'backupMediaBatch', - httpType: 'PUT', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - headers, - responseType: 'json', - jsonData: { - items: items.map(item => { - const { - sourceAttachment, - objectLength, - mediaId, - hmacKey, - encryptionKey, - } = item; - - return { - sourceAttachment: { - cdn: sourceAttachment.cdn, - key: sourceAttachment.key, - }, - objectLength, - mediaId, - hmacKey: Bytes.toBase64(hmacKey), - encryptionKey: Bytes.toBase64(encryptionKey), - }; - }), - }, - zodSchema: backupMediaBatchResponseSchema, - }); - } - - async function backupDeleteMedia({ - headers, - mediaToDelete, - }: BackupDeleteMediaOptionsType) { - await _ajax({ - host: 'chatService', - call: 'backupMediaDelete', - httpType: 'POST', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - headers, - jsonData: { - mediaToDelete: mediaToDelete.map(({ cdn, mediaId }) => { - return { - cdn, - mediaId, - }; - }), - }, - }); - } - - async function backupListMedia({ - headers, - cursor, - limit, - }: BackupListMediaOptionsType) { - const params = new Array(); - - if (cursor != null) { - params.push(`cursor=${encodeURIComponent(cursor)}`); - } - params.push(`limit=${limit}`); - - return _ajax({ - host: 'chatService', - call: 'backupMedia', - httpType: 'GET', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - headers, - responseType: 'json', - urlParameters: `?${params.join('&')}`, - zodSchema: backupListMediaResponseSchema, - }); - } - - async function callLinkCreateAuth( - requestBase64: string - ): Promise { - return _ajax({ - host: 'chatService', - call: 'callLinkCreateAuth', - httpType: 'POST', - responseType: 'json', - jsonData: { createCallLinkCredentialRequest: requestBase64 }, - zodSchema: callLinkCreateAuthResponseSchema, - }); - } - - async function setPhoneNumberDiscoverability(newValue: boolean) { - await _ajax({ - host: 'chatService', - call: 'phoneNumberDiscoverability', - httpType: 'PUT', - jsonData: { - discoverableByPhoneNumber: newValue, - }, - }); - } - - async function getMyKeyCounts( - serviceIdKind: ServiceIdKind - ): Promise> { - return _ajax({ - host: 'chatService', - call: 'keys', - urlParameters: `?${serviceIdKindToQuery(serviceIdKind)}`, - httpType: 'GET', - responseType: 'json', - zodSchema: ServerKeyCountSchema, - }); - } - - function handleKeys(res: ServerKeyResponseType): ServerKeysType { - if (!Array.isArray(res.devices)) { - throw new Error('Invalid response'); - } - - const devices = res.devices.map(device => { - if ( - !_validateResponse(device, { signedPreKey: 'object' }) || - !_validateResponse(device.signedPreKey, { - publicKey: 'string', - signature: 'string', - }) - ) { - throw new Error('Invalid signedPreKey'); } + ) + ) + ); +} - let preKey; - if (device.preKey) { - if ( - !_validateResponse(device, { preKey: 'object' }) || - !_validateResponse(device.preKey, { publicKey: 'string' }) - ) { - throw new Error('Invalid preKey'); - } +export async function getSubscriptionConfiguration(): Promise { + return _ajax({ + host: 'chatService', + call: 'subscriptionConfiguration', + httpType: 'GET', + responseType: 'json', + zodSchema: subscriptionConfigurationResultZod, + }); +} - preKey = { - keyId: device.preKey.keyId, - publicKey: Bytes.fromBase64(device.preKey.publicKey), - }; - } +export async function getAvatar(path: string): Promise { + // Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our + // attachment CDN, it uses our self-signed certificate, so we pass it in. + return _outerAjax(`${cdnUrlObject['0']}/${path}`, { + certificateAuthority, + contentType: 'application/octet-stream', + proxyUrl, + responseType: 'bytes', + timeout: 90 * SECOND, + type: 'GET', + redactUrl: (href: string) => { + const pattern = RegExp(escapeRegExp(path), 'g'); + return href.replace(pattern, `[REDACTED]${path.slice(-3)}`); + }, + version, + }); +} - return { - deviceId: device.deviceId, - registrationId: device.registrationId, - ...(preKey ? { preKey } : null), - ...(device.signedPreKey - ? { - signedPreKey: { - keyId: device.signedPreKey.keyId, - publicKey: Bytes.fromBase64(device.signedPreKey.publicKey), - signature: Bytes.fromBase64(device.signedPreKey.signature), - }, - } - : null), - ...(device.pqPreKey - ? { - pqPreKey: { - keyId: device.pqPreKey.keyId, - publicKey: Bytes.fromBase64(device.pqPreKey.publicKey), - signature: Bytes.fromBase64(device.pqPreKey.signature), - }, - } - : null), - }; - }); +export async function deleteUsername(abortSignal?: AbortSignal): Promise { + await _ajax({ + host: 'chatService', + call: 'username', + httpType: 'DELETE', + abortSignal, + }); +} - return { - devices, - identityKey: Bytes.fromBase64(res.identityKey), - }; +export async function reserveUsername({ + hashes, + abortSignal, +}: ReserveUsernameOptionsType): Promise { + return _ajax({ + host: 'chatService', + call: 'reserveUsername', + httpType: 'PUT', + jsonData: { + usernameHashes: hashes.map(hash => toWebSafeBase64(Bytes.toBase64(hash))), + }, + responseType: 'json', + abortSignal, + zodSchema: reserveUsernameResultZod, + }); +} +export async function confirmUsername({ + hash, + proof, + encryptedUsername, + abortSignal, +}: ConfirmUsernameOptionsType): Promise { + return _ajax({ + host: 'chatService', + call: 'confirmUsername', + httpType: 'PUT', + jsonData: { + usernameHash: toWebSafeBase64(Bytes.toBase64(hash)), + zkProof: toWebSafeBase64(Bytes.toBase64(proof)), + encryptedUsername: toWebSafeBase64(Bytes.toBase64(encryptedUsername)), + }, + responseType: 'json', + abortSignal, + zodSchema: confirmUsernameResultZod, + }); +} + +export async function replaceUsernameLink({ + encryptedUsername, + keepLinkHandle, +}: ReplaceUsernameLinkOptionsType): Promise { + return _ajax({ + host: 'chatService', + call: 'usernameLink', + httpType: 'PUT', + responseType: 'json', + jsonData: { + usernameLinkEncryptedValue: toWebSafeBase64( + Bytes.toBase64(encryptedUsername) + ), + keepLinkHandle, + }, + zodSchema: replaceUsernameLinkResultZod, + }); +} + +export async function deleteUsernameLink(): Promise { + await _ajax({ + host: 'chatService', + call: 'usernameLink', + httpType: 'DELETE', + }); +} + +export async function resolveUsernameLink( + serverId: string +): Promise { + return _ajax({ + host: 'chatService', + httpType: 'GET', + call: 'usernameLink', + urlParameters: `/${encodeURIComponent(serverId)}`, + responseType: 'json', + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + zodSchema: resolveUsernameLinkResultZod, + }); +} + +export async function reportMessage({ + senderAci, + serverGuid, + token, +}: ReportMessageOptionsType): Promise { + const jsonData = { token }; + + await _ajax({ + host: 'chatService', + call: 'reportMessage', + httpType: 'POST', + urlParameters: urlPathFromComponents([senderAci, serverGuid]), + responseType: 'bytes', + jsonData, + }); +} + +export async function requestVerification( + number: string, + captcha: string, + transport: VerificationTransport +): Promise { + // Create a new blank session using just a E164 + const session = await libsignalNet.createRegistrationSession({ + e164: number, + }); + + // Submit a captcha solution to the session + await session.submitCaptcha(captcha); + + // Verify that captcha was accepted + if (!session.sessionState.allowedToRequestCode) { + throw new Error('requestVerification: Not allowed to send code'); + } + + // Request an SMS or Voice confirmation + await session.requestVerification({ + transport: transport === VerificationTransport.SMS ? 'sms' : 'voice', + client: 'ios', + languages: [], + }); + + // Return sessionId to be used in `createAccount` + return { sessionId: session.sessionId }; +} + +export async function checkAccountExistence( + serviceId: ServiceIdString +): Promise { + try { + await _ajax({ + host: 'chatService', + httpType: 'HEAD', + call: 'accountExistence', + urlParameters: `/${serviceId}`, + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + }); + return true; + } catch (error) { + if (error instanceof HTTPError && error.code === 404) { + return false; } - async function getKeysForServiceId( - serviceId: ServiceIdString, - deviceId?: number - ) { - const keys = await _ajax({ - host: 'chatService', - call: 'keys', - httpType: 'GET', - urlParameters: `/${serviceId}/${deviceId || '*'}`, - responseType: 'json', - zodSchema: ServerKeyResponseSchema, - }); - return handleKeys(keys); - } + throw error; + } +} - async function getKeysForServiceIdUnauth( - serviceId: ServiceIdString, - deviceId?: number, - { - accessKey, - groupSendToken, - }: { accessKey?: string; groupSendToken?: GroupSendToken } = {} - ) { - const keys = await _ajax({ - host: 'chatService', - call: 'keys', - httpType: 'GET', - urlParameters: `/${serviceId}/${deviceId || '*'}`, - responseType: 'json', - unauthenticated: true, - accessKey, - groupSendToken, - zodSchema: ServerKeyResponseSchema, - }); - return handleKeys(keys); - } +export function startRegistration(): unknown { + strictAssert( + activeRegistration === undefined, + 'Registration already in progress' + ); - async function sendMessagesUnauth( - destination: ServiceIdString, - messages: ReadonlyArray, - timestamp: number, - { - accessKey, - groupSendToken, - online, - urgent = true, - story = false, - }: { - accessKey: string | null; - groupSendToken: GroupSendToken | null; - online?: boolean; - story?: boolean; - urgent?: boolean; - } - ) { - const jsonData = { - messages, - timestamp, - online: Boolean(online), - urgent, - }; + activeRegistration = explodePromise(); + log.info('starting registration'); - await _ajax({ - host: 'chatService', - call: 'messages', - httpType: 'PUT', - urlParameters: `/${destination}?story=${booleanToString(story)}`, - jsonData, - responseType: 'json', - unauthenticated: true, - accessKey: accessKey ?? undefined, - groupSendToken: groupSendToken ?? undefined, - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - }); - } + return activeRegistration; +} - async function sendMessages( - destination: ServiceIdString, - messages: ReadonlyArray, - timestamp: number, - { - online, - urgent = true, - story = false, - }: { online?: boolean; story?: boolean; urgent?: boolean } - ) { - const jsonData = { - messages, - timestamp, - online: Boolean(online), - urgent, - }; +export function finishRegistration(registration: unknown): void { + strictAssert(activeRegistration !== undefined, 'No active registration'); + strictAssert( + activeRegistration === registration, + 'Invalid registration baton' + ); - await _ajax({ - host: 'chatService', - call: 'messages', - httpType: 'PUT', - urlParameters: `/${destination}?story=${booleanToString(story)}`, - jsonData, - responseType: 'json', - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - }); - } + log.info('finishing registration'); + const current = activeRegistration; + activeRegistration = undefined; + current.resolve(); +} - function booleanToString(value: boolean | undefined): string { - return value ? 'true' : 'false'; - } +async function _withNewCredentials< + Result extends { uuid: AciString; deviceId?: number }, +>( + { username: newUsername, password: newPassword }: WebAPICredentials, + callback: () => Promise +): Promise { + // Reset old websocket credentials and disconnect. + // AccountManager is our only caller and it will trigger + // `registration_done` which will update credentials. + await logout(); - async function sendWithSenderKey( - data: Uint8Array, - accessKeys: Uint8Array | null, - groupSendToken: GroupSendToken | null, - timestamp: number, - { - online, - urgent = true, - story = false, - }: { - online?: boolean; - story?: boolean; - urgent?: boolean; - } - ): Promise { - const onlineParam = `&online=${booleanToString(online)}`; - const urgentParam = `&urgent=${booleanToString(urgent)}`; - const storyParam = `&story=${booleanToString(story)}`; + // Update REST credentials, though. We need them for the call below + username = newUsername; + password = newPassword; + const result = await callback(); + + const { uuid: aci = newUsername, deviceId = 1 } = result; + + // Set final REST credentials to let `registerKeys` succeed. + username = `${aci}.${deviceId}`; + password = newPassword; + + return result; +} + +export async function createAccount({ + sessionId, + number, + code, + newPassword, + registrationId, + pniRegistrationId, + accessKey, + aciPublicKey, + pniPublicKey, + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, +}: CreateAccountOptionsType): Promise { + const session = await libsignalNet.resumeRegistrationSession({ + sessionId, + e164: number, + }); + const verified = await session.verifySession(code); + + if (!verified) { + throw new Error('createAccount: invalid code'); + } + + const capabilities: CapabilitiesUploadType = { + attachmentBackfill: true, + spqr: true, + }; + + // Desktop doesn't support recovery but we need to provide a recovery password. + // Since the value isn't used, just use a random one and then throw it away. + const recoveryPassword = getRandomBytes(32); + const accountAttributes = new AccountAttributes({ + aciRegistrationId: registrationId, + pniRegistrationId, + capabilities: new Set( + Object.entries(capabilities).flatMap(([k, v]) => (v ? [k] : [])) + ), + unidentifiedAccessKey: accessKey, + unrestrictedUnidentifiedAccess: false, + recoveryPassword, + registrationLock: null, + discoverableByPhoneNumber: false, + }); + + // Massages UploadSignedPreKey into SignedPublicPreKey and likewise for Kyber. + function asSignedKey(key: { + keyId: number; + publicKey: K; + signature: Uint8Array; + }): { + id: () => number; + publicKey: () => K; + signature: () => Uint8Array; + } { + return { + id: () => key.keyId, + signature: () => key.signature, + publicKey: () => key.publicKey, + }; + } + + const { aci, pni } = await session.registerAccount({ + accountPassword: newPassword, + accountAttributes, + skipDeviceTransfer: true, + aciPublicKey, + pniPublicKey, + aciSignedPreKey: asSignedKey(aciSignedPreKey), + pniSignedPreKey: asSignedKey(pniSignedPreKey), + aciPqLastResortPreKey: asSignedKey(aciPqLastResortPreKey), + pniPqLastResortPreKey: asSignedKey(pniPqLastResortPreKey), + }); + + return { aci, pni }; +} + +export async function linkDevice({ + number, + verificationCode, + encryptedDeviceName, + newPassword, + registrationId, + pniRegistrationId, + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, +}: LinkDeviceOptionsType): Promise { + const capabilities: CapabilitiesUploadType = { + attachmentBackfill: true, + spqr: true, + }; + + const jsonData = { + verificationCode, + accountAttributes: { + fetchesMessages: true, + name: encryptedDeviceName, + registrationId, + pniRegistrationId, + capabilities, + }, + aciSignedPreKey: serializeSignedPreKey(aciSignedPreKey), + pniSignedPreKey: serializeSignedPreKey(pniSignedPreKey), + aciPqLastResortPreKey: serializeSignedPreKey(aciPqLastResortPreKey), + pniPqLastResortPreKey: serializeSignedPreKey(pniPqLastResortPreKey), + }; + return _withNewCredentials( + { + username: number, + password: newPassword, + }, + async () => { const response = await _ajax({ host: 'chatService', - call: 'multiRecipient', + isRegistration: true, + call: 'linkDevice', httpType: 'PUT', - contentType: 'application/vnd.signal-messenger.mrm', - data, - urlParameters: `?ts=${timestamp}${onlineParam}${urgentParam}${storyParam}`, responseType: 'json', - unauthenticated: true, - accessKey: accessKeys != null ? Bytes.toBase64(accessKeys) : undefined, - groupSendToken: groupSendToken ?? undefined, - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - }); - const parseResult = safeParseUnknown( - multiRecipient200ResponseSchema, - response - ); - if (parseResult.success) { - return parseResult.data; - } - - log.error( - 'invalid response from sendWithSenderKey', - toLogFormat(parseResult.error) - ); - return response as MultiRecipient200ResponseType; - } - - function redactStickerUrl(stickerUrl: string) { - return stickerUrl.replace( - /(\/stickers\/)([^/]+)(\/)/, - (_, begin: string, packId: string, end: string) => - `${begin}${redactPackId(packId)}${end}` - ); - } - - async function getSticker(packId: string, stickerId: number) { - if (!isPackIdValid(packId)) { - throw new Error('getSticker: pack ID was invalid'); - } - return _outerAjax( - `${cdnUrlObject['0']}/stickers/${packId}/full/${stickerId}`, - { - certificateAuthority, - proxyUrl, - responseType: 'bytes', - type: 'GET', - redactUrl: redactStickerUrl, - version, - } - ); - } - - async function getStickerPackManifest(packId: string) { - if (!isPackIdValid(packId)) { - throw new Error('getStickerPackManifest: pack ID was invalid'); - } - return _outerAjax( - `${cdnUrlObject['0']}/stickers/${packId}/manifest.proto`, - { - certificateAuthority, - proxyUrl, - responseType: 'bytes', - type: 'GET', - redactUrl: redactStickerUrl, - version, - } - ); - } - - type ServerV2AttachmentType = { - key: string; - credential: string; - acl: string; - algorithm: string; - date: string; - policy: string; - signature: string; - }; - - function makePutParams( - { - key, - credential, - acl, - algorithm, - date, - policy, - signature, - }: ServerV2AttachmentType, - encryptedBin: Uint8Array - ) { - // Note: when using the boundary string in the POST body, it needs to be prefixed by - // an extra --, and the final boundary string at the end gets a -- prefix and a -- - // suffix. - const boundaryString = `----------------${getGuid().replace(/-/g, '')}`; - const CRLF = '\r\n'; - const getSection = (name: string, value: string) => - [ - `--${boundaryString}`, - `Content-Disposition: form-data; name="${name}"${CRLF}`, - value, - ].join(CRLF); - - const start = [ - getSection('key', key), - getSection('x-amz-credential', credential), - getSection('acl', acl), - getSection('x-amz-algorithm', algorithm), - getSection('x-amz-date', date), - getSection('policy', policy), - getSection('x-amz-signature', signature), - getSection('Content-Type', 'application/octet-stream'), - `--${boundaryString}`, - 'Content-Disposition: form-data; name="file"', - `Content-Type: application/octet-stream${CRLF}${CRLF}`, - ].join(CRLF); - const end = `${CRLF}--${boundaryString}--${CRLF}`; - - const startBuffer = Bytes.fromString(start); - const attachmentBuffer = encryptedBin; - const endBuffer = Bytes.fromString(end); - - const data = Bytes.concatenate([ - startBuffer, - attachmentBuffer, - endBuffer, - ]); - - return { - data, - contentType: `multipart/form-data; boundary=${boundaryString}`, - headers: { - 'Content-Length': data.length.toString(), - }, - }; - } - - async function putStickers( - encryptedManifest: Uint8Array, - encryptedStickers: ReadonlyArray, - onProgress?: () => void - ) { - // Get manifest and sticker upload parameters - const { packId, manifest, stickers } = await _ajax({ - host: 'chatService', - call: 'getStickerPackUpload', - responseType: 'json', - httpType: 'GET', - urlParameters: `/${encryptedStickers.length}`, - zodSchema: StickerPackUploadFormSchema, + jsonData, + zodSchema: linkDeviceResultZod, }); - // Upload manifest - const manifestParams = makePutParams(manifest, encryptedManifest); - // This is going to the CDN, not the service, so we use _outerAjax - await _outerAjax(`${cdnUrlObject['0']}/`, { - ...manifestParams, - certificateAuthority, - proxyUrl, - timeout: 0, - type: 'POST', - version, - responseType: 'raw', - }); - - // Upload stickers - const queue = new PQueue({ - concurrency: 3, - timeout: MINUTE * 30, - throwOnTimeout: true, - }); - await Promise.all( - stickers.map(async (sticker: ServerV2AttachmentType, index: number) => { - const stickerParams = makePutParams( - sticker, - encryptedStickers[index] - ); - await queue.add(async () => - _outerAjax(`${cdnUrlObject['0']}/`, { - ...stickerParams, - certificateAuthority, - proxyUrl, - timeout: 0, - type: 'POST', - version, - responseType: 'raw', - }) - ); - if (onProgress) { - onProgress(); - } - }) - ); - - // Done! - return packId; + return response; } + ); +} - // Transit tier is the default place for normal (non-backup) attachments. - // Called "transit" because it is transitory - async function getAttachment({ - cdnKey, - cdnNumber, - options, - }: { - cdnKey: string; - cdnNumber?: number; - options?: { - disableRetries?: boolean; - timeout?: number; - downloadOffset?: number; - abortSignal?: AbortSignal; - }; - }) { - return _getAttachment({ - cdnPath: `/attachments/${cdnKey}`, - cdnNumber: cdnNumber ?? 0, - redactor: _createRedactor(cdnKey), - options, - }); - } +export async function unlink(): Promise { + if (!username) { + return; + } - async function getAttachmentFromBackupTier({ - mediaId, - backupDir, - mediaDir, - cdnNumber, - headers, - options, - }: GetAttachmentFromBackupTierArgsType) { - return _getAttachment({ - cdnPath: urlPathFromComponents([ - 'backups', - backupDir, - mediaDir, + const [, deviceId] = username.split('.'); + await _ajax({ + host: 'chatService', + call: 'devices', + httpType: 'DELETE', + urlParameters: `/${deviceId}`, + }); +} + +export async function getDevices(): Promise { + return _ajax({ + host: 'chatService', + call: 'devices', + httpType: 'GET', + responseType: 'json', + zodSchema: getDevicesResultZod, + }); +} + +export async function updateDeviceName(deviceName: string): Promise { + await _ajax({ + host: 'chatService', + call: 'updateDeviceName', + httpType: 'PUT', + jsonData: { + deviceName, + }, + }); +} + +export async function getIceServers(): Promise { + return (await _ajax({ + host: 'chatService', + call: 'getIceServers', + httpType: 'GET', + responseType: 'json', + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + })) as GetIceServersResultType; +} + +type JSONSignedPreKeyType = { + keyId: number; + publicKey: string; + signature: string; +}; +type JSONPreKeyType = { + keyId: number; + publicKey: string; +}; +type JSONKyberPreKeyType = { + keyId: number; + publicKey: string; + signature: string; +}; + +type JSONKeysType = { + preKeys?: Array; + pqPreKeys?: Array; + pqLastResortPreKey?: JSONKyberPreKeyType; + signedPreKey?: JSONSignedPreKeyType; +}; + +export async function registerKeys( + genKeys: UploadKeysType, + serviceIdKind: ServiceIdKind +): Promise { + const preKeys = genKeys.preKeys?.map(key => ({ + keyId: key.keyId, + publicKey: Bytes.toBase64(key.publicKey.serialize()), + })); + const pqPreKeys = genKeys.pqPreKeys?.map(key => ({ + keyId: key.keyId, + publicKey: Bytes.toBase64(key.publicKey.serialize()), + signature: Bytes.toBase64(key.signature), + })); + + if ( + !preKeys?.length && + !pqPreKeys?.length && + !genKeys.pqLastResortPreKey && + !genKeys.signedPreKey + ) { + throw new Error( + 'registerKeys: None of the four potential key types were provided!' + ); + } + if (preKeys && preKeys.length === 0) { + throw new Error('registerKeys: Attempting to upload zero preKeys!'); + } + if (pqPreKeys && pqPreKeys.length === 0) { + throw new Error('registerKeys: Attempting to upload zero pqPreKeys!'); + } + + const keys: JSONKeysType = { + preKeys, + pqPreKeys, + pqLastResortPreKey: serializeSignedPreKey(genKeys.pqLastResortPreKey), + signedPreKey: serializeSignedPreKey(genKeys.signedPreKey), + }; + + await _ajax({ + host: 'chatService', + isRegistration: true, + call: 'keys', + urlParameters: `?${serviceIdKindToQuery(serviceIdKind)}`, + httpType: 'PUT', + jsonData: keys, + }); +} + +export async function getBackupInfo( + headers: BackupPresentationHeadersType +): Promise { + return _ajax({ + host: 'chatService', + call: 'backup', + httpType: 'GET', + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + headers, + responseType: 'json', + zodSchema: getBackupInfoResponseSchema, + }); +} + +export async function getBackupStream({ + headers, + cdn, + backupDir, + backupName, + downloadOffset, + onProgress, + abortSignal, +}: GetBackupStreamOptionsType): Promise { + return _getAttachment({ + cdnPath: `/backups/${encodeURIComponent(backupDir)}/${encodeURIComponent(backupName)}`, + cdnNumber: cdn, + redactor: _createRedactor(backupDir, backupName), + headers, + options: { + downloadOffset, + onProgress, + abortSignal, + }, + }); +} +export async function getBackupFileHeaders({ + headers, + cdn, + backupDir, + backupName, +}: Pick< + GetBackupStreamOptionsType, + 'headers' | 'cdn' | 'backupDir' | 'backupName' +>): Promise { + const result = await _getAttachmentHeaders({ + cdnPath: `/backups/${encodeURIComponent(backupDir)}/${encodeURIComponent(backupName)}`, + cdnNumber: cdn, + redactor: _createRedactor(backupDir, backupName), + headers, + }); + const responseHeaders = Object.fromEntries(result.entries()); + + return parseUnknown(backupFileHeadersSchema, responseHeaders as unknown); +} + +export async function getEphemeralBackupStream({ + cdn, + key, + downloadOffset, + onProgress, + abortSignal, +}: GetEphemeralBackupStreamOptionsType): Promise { + return _getAttachment({ + cdnNumber: cdn, + cdnPath: `/attachments/${encodeURIComponent(key)}`, + redactor: _createRedactor(key), + options: { + downloadOffset, + onProgress, + abortSignal, + }, + }); +} + +export async function getBackupMediaUploadForm( + headers: BackupPresentationHeadersType +): Promise { + return _ajax({ + host: 'chatService', + call: 'getBackupMediaUploadForm', + httpType: 'GET', + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + headers, + responseType: 'json', + zodSchema: attachmentUploadFormResponse, + }); +} + +export function createFetchForAttachmentUpload({ + signedUploadLocation, + headers: uploadHeaders, + cdn, +}: AttachmentUploadFormResponseType): FetchFunctionType { + strictAssert(cdn === 3, 'Fetch can only be created for CDN 3'); + const { origin: expectedOrigin } = new URL(signedUploadLocation); + + return async ( + endpoint: string | URL, + init: RequestInit + ): Promise => { + const { origin } = new URL(endpoint); + strictAssert(origin === expectedOrigin, `Unexpected origin: ${origin}`); + + const fetchOptions = await getFetchOptions({ + // Will be overriden + type: 'GET', + + certificateAuthority, + proxyUrl, + timeout: 0, + version, + + headers: uploadHeaders, + }); + + return fetch(endpoint, { + ...fetchOptions, + ...init, + headers: { + ...fetchOptions.headers, + ...init.headers, + }, + }); + }; +} + +export async function getBackupUploadForm( + headers: BackupPresentationHeadersType +): Promise { + return _ajax({ + host: 'chatService', + call: 'getBackupUploadForm', + httpType: 'GET', + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + headers, + responseType: 'json', + zodSchema: attachmentUploadFormResponse, + }); +} + +export async function refreshBackup( + headers: BackupPresentationHeadersType +): Promise { + await _ajax({ + host: 'chatService', + call: 'backup', + httpType: 'POST', + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + headers, + }); +} + +export async function getBackupCredentials({ + startDayInMs, + endDayInMs, +}: GetBackupCredentialsOptionsType): Promise { + const startDayInSeconds = startDayInMs / SECOND; + const endDayInSeconds = endDayInMs / SECOND; + return _ajax({ + host: 'chatService', + call: 'getBackupCredentials', + httpType: 'GET', + urlParameters: + `?redemptionStartSeconds=${startDayInSeconds}&` + + `redemptionEndSeconds=${endDayInSeconds}`, + responseType: 'json', + zodSchema: getBackupCredentialsResponseSchema, + }); +} + +export async function getBackupCDNCredentials({ + headers, + cdnNumber, +}: GetBackupCDNCredentialsOptionsType): Promise { + return _ajax({ + host: 'chatService', + call: 'getBackupCDNCredentials', + httpType: 'GET', + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + headers, + urlParameters: `?cdn=${cdnNumber}`, + responseType: 'json', + zodSchema: getBackupCDNCredentialsResponseSchema, + }); +} + +export async function setBackupId({ + messagesBackupAuthCredentialRequest, + mediaBackupAuthCredentialRequest, +}: SetBackupIdOptionsType): Promise { + await _ajax({ + host: 'chatService', + call: 'setBackupId', + httpType: 'PUT', + jsonData: { + messagesBackupAuthCredentialRequest: Bytes.toBase64( + messagesBackupAuthCredentialRequest + ), + mediaBackupAuthCredentialRequest: Bytes.toBase64( + mediaBackupAuthCredentialRequest + ), + }, + }); +} + +export async function setBackupSignatureKey({ + headers, + backupIdPublicKey, +}: SetBackupSignatureKeyOptionsType): Promise { + await _ajax({ + host: 'chatService', + call: 'setBackupSignatureKey', + httpType: 'PUT', + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + headers, + jsonData: { + backupIdPublicKey: Bytes.toBase64(backupIdPublicKey), + }, + }); +} + +export async function backupMediaBatch({ + headers, + items, +}: BackupMediaBatchOptionsType): Promise { + return _ajax({ + host: 'chatService', + call: 'backupMediaBatch', + httpType: 'PUT', + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + headers, + responseType: 'json', + jsonData: { + items: items.map(item => { + const { + sourceAttachment, + objectLength, mediaId, - ]), - cdnNumber, - headers, - redactor: _createRedactor(backupDir, mediaDir, mediaId), - options, - }); - } + hmacKey, + encryptionKey, + } = item; - function getCheckedCdnUrl(cdnNumber: number, cdnPath: string) { - const baseUrl = cdnUrlObject[cdnNumber] ?? cdnUrlObject['0']; - const { origin: expectedOrigin } = new URL(baseUrl); - const fullCdnUrl = `${baseUrl}${cdnPath}`; - const { origin } = new URL(fullCdnUrl); + return { + sourceAttachment: { + cdn: sourceAttachment.cdn, + key: sourceAttachment.key, + }, + objectLength, + mediaId, + hmacKey: Bytes.toBase64(hmacKey), + encryptionKey: Bytes.toBase64(encryptionKey), + }; + }), + }, + zodSchema: backupMediaBatchResponseSchema, + }); +} - strictAssert(origin === expectedOrigin, `Unexpected origin: ${origin}`); - return fullCdnUrl; - } +export async function backupDeleteMedia({ + headers, + mediaToDelete, +}: BackupDeleteMediaOptionsType): Promise { + await _ajax({ + host: 'chatService', + call: 'backupMediaDelete', + httpType: 'POST', + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + headers, + jsonData: { + mediaToDelete: mediaToDelete.map(({ cdn, mediaId }) => { + return { + cdn, + mediaId, + }; + }), + }, + }); +} - async function _getAttachmentHeaders({ - cdnPath, - cdnNumber, - headers = {}, - redactor, - }: Omit): Promise { - const fullCdnUrl = getCheckedCdnUrl(cdnNumber, cdnPath); - const response = await _outerAjax(fullCdnUrl, { - headers, - certificateAuthority, - proxyUrl, - responseType: 'raw', - timeout: DEFAULT_TIMEOUT, - type: 'HEAD', - redactUrl: redactor, - version, - }); - return response.headers; - } +export async function backupListMedia({ + headers, + cursor, + limit, +}: BackupListMediaOptionsType): Promise { + const params = new Array(); - async function _getAttachment({ - cdnPath, - cdnNumber, - headers = {}, - redactor, - options, - }: GetAttachmentArgsType): Promise { - const abortController = new AbortController(); + if (cursor != null) { + params.push(`cursor=${encodeURIComponent(cursor)}`); + } + params.push(`limit=${limit}`); - let streamWithDetails: StreamWithDetailsType | undefined; + return _ajax({ + host: 'chatService', + call: 'backupMedia', + httpType: 'GET', + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + headers, + responseType: 'json', + urlParameters: `?${params.join('&')}`, + zodSchema: backupListMediaResponseSchema, + }); +} - const cancelRequest = (reason: unknown) => { - abortController.abort(reason); - }; +export async function callLinkCreateAuth( + requestBase64: string +): Promise { + return _ajax({ + host: 'chatService', + call: 'callLinkCreateAuth', + httpType: 'POST', + responseType: 'json', + jsonData: { createCallLinkCredentialRequest: requestBase64 }, + zodSchema: callLinkCreateAuthResponseSchema, + }); +} - options?.abortSignal?.addEventListener('abort', () => - cancelRequest(options.abortSignal?.reason) - ); +export async function setPhoneNumberDiscoverability( + newValue: boolean +): Promise { + await _ajax({ + host: 'chatService', + call: 'phoneNumberDiscoverability', + httpType: 'PUT', + jsonData: { + discoverableByPhoneNumber: newValue, + }, + }); +} - registerInflightRequest(cancelRequest); +export async function getMyKeyCounts( + serviceIdKind: ServiceIdKind +): Promise> { + return _ajax({ + host: 'chatService', + call: 'keys', + urlParameters: `?${serviceIdKindToQuery(serviceIdKind)}`, + httpType: 'GET', + responseType: 'json', + zodSchema: ServerKeyCountSchema, + }); +} - let totalBytes = 0; +function handleKeys(res: ServerKeyResponseType): ServerKeysType { + if (!Array.isArray(res.devices)) { + throw new Error('Invalid response'); + } - // This is going to the CDN, not the service, so we use _outerAjax - try { - const targetHeaders = { ...headers }; - if (options?.downloadOffset) { - targetHeaders.range = `bytes=${options.downloadOffset}-`; - } - const fullCdnUrl = getCheckedCdnUrl(cdnNumber, cdnPath); - - streamWithDetails = await _outerAjax(fullCdnUrl, { - headers: targetHeaders, - certificateAuthority, - disableRetries: options?.disableRetries, - proxyUrl, - responseType: 'streamwithdetails', - timeout: options?.timeout ?? DEFAULT_TIMEOUT, - type: 'GET', - redactUrl: redactor, - version, - abortSignal: abortController.signal, - }); - - if (targetHeaders.range == null) { - const contentLength = - streamWithDetails.response.headers.get('content-length'); - strictAssert( - contentLength != null, - 'Attachment Content-Length is absent' - ); - - const maybeSize = safeParseNumber(contentLength); - strictAssert( - maybeSize != null, - 'Attachment Content-Length is not a number' - ); - - totalBytes = maybeSize; - } else { - strictAssert( - streamWithDetails.response.status === 206, - `Expected 206 status code for offset ${options?.downloadOffset}` - ); - strictAssert( - !streamWithDetails.contentType?.includes('multipart'), - 'Expected non-multipart response' - ); - - const range = streamWithDetails.response.headers.get('content-range'); - strictAssert(range != null, 'Attachment Content-Range is absent'); - - const match = PARSE_RANGE_HEADER.exec(range); - strictAssert(match != null, 'Attachment Content-Range is invalid'); - const maybeSize = safeParseNumber(match[1]); - strictAssert( - maybeSize != null, - 'Attachment Content-Range[1] is not a number' - ); - - totalBytes = maybeSize; - } - } finally { - if (!streamWithDetails) { - unregisterInFlightRequest(cancelRequest); - } else { - streamWithDetails.stream.on('close', () => { - unregisterInFlightRequest(cancelRequest); - }); - } - } - - const timeoutStream = getTimeoutStream({ - name: `getAttachment(${redactor(cdnPath)})`, - timeout: GET_ATTACHMENT_CHUNK_TIMEOUT, - abortController, - }); - - const combinedStream = streamWithDetails.stream - // We do this manually; pipe() doesn't flow errors through the streams for us - .on('error', (error: Error) => { - timeoutStream.emit('error', error); - }) - .pipe(timeoutStream); - - if (options?.onProgress) { - const { onProgress } = options; - let currentBytes = options.downloadOffset ?? 0; - - combinedStream.pause(); - combinedStream.on('data', chunk => { - currentBytes += chunk.byteLength; - onProgress(currentBytes, totalBytes); - }); - onProgress(0, totalBytes); - } - - return combinedStream; - } - - async function getAttachmentUploadForm(): Promise { - return _ajax({ - host: 'chatService', - call: 'attachmentUploadForm', - httpType: 'GET', - responseType: 'json', - zodSchema: attachmentUploadFormResponse, - }); - } - - async function putEncryptedAttachment( - encryptedBin: (start: number, end?: number) => Readable, - encryptedSize: number, - uploadForm: AttachmentUploadFormResponseType + const devices = res.devices.map(device => { + if ( + !_validateResponse(device, { signedPreKey: 'object' }) || + !_validateResponse(device.signedPreKey, { + publicKey: 'string', + signature: 'string', + }) ) { - const { signedUploadLocation, headers } = uploadForm; + throw new Error('Invalid signedPreKey'); + } - // This is going to the CDN, not the service, so we use _outerAjax - const { response: uploadResponse } = await _outerAjax( - signedUploadLocation, - { - responseType: 'byteswithdetails', + let preKey; + if (device.preKey) { + if ( + !_validateResponse(device, { preKey: 'object' }) || + !_validateResponse(device.preKey, { publicKey: 'string' }) + ) { + throw new Error('Invalid preKey'); + } + + preKey = { + keyId: device.preKey.keyId, + publicKey: Bytes.fromBase64(device.preKey.publicKey), + }; + } + + return { + deviceId: device.deviceId, + registrationId: device.registrationId, + ...(preKey ? { preKey } : null), + ...(device.signedPreKey + ? { + signedPreKey: { + keyId: device.signedPreKey.keyId, + publicKey: Bytes.fromBase64(device.signedPreKey.publicKey), + signature: Bytes.fromBase64(device.signedPreKey.signature), + }, + } + : null), + ...(device.pqPreKey + ? { + pqPreKey: { + keyId: device.pqPreKey.keyId, + publicKey: Bytes.fromBase64(device.pqPreKey.publicKey), + signature: Bytes.fromBase64(device.pqPreKey.signature), + }, + } + : null), + }; + }); + + return { + devices, + identityKey: Bytes.fromBase64(res.identityKey), + }; +} + +export async function getKeysForServiceId( + serviceId: ServiceIdString, + deviceId?: number +): Promise { + const keys = await _ajax({ + host: 'chatService', + call: 'keys', + httpType: 'GET', + urlParameters: `/${serviceId}/${deviceId || '*'}`, + responseType: 'json', + zodSchema: ServerKeyResponseSchema, + }); + return handleKeys(keys); +} + +export async function getKeysForServiceIdUnauth( + serviceId: ServiceIdString, + deviceId?: number, + { + accessKey, + groupSendToken, + }: { accessKey?: string; groupSendToken?: GroupSendToken } = {} +): Promise { + const keys = await _ajax({ + host: 'chatService', + call: 'keys', + httpType: 'GET', + urlParameters: `/${serviceId}/${deviceId || '*'}`, + responseType: 'json', + unauthenticated: true, + accessKey, + groupSendToken, + zodSchema: ServerKeyResponseSchema, + }); + return handleKeys(keys); +} + +export async function sendMessagesUnauth( + destination: ServiceIdString, + messages: ReadonlyArray, + timestamp: number, + { + accessKey, + groupSendToken, + online, + urgent = true, + story = false, + }: { + accessKey: string | null; + groupSendToken: GroupSendToken | null; + online?: boolean; + story?: boolean; + urgent?: boolean; + } +): Promise { + const jsonData = { + messages, + timestamp, + online: Boolean(online), + urgent, + }; + + await _ajax({ + host: 'chatService', + call: 'messages', + httpType: 'PUT', + urlParameters: `/${destination}?story=${booleanToString(story)}`, + jsonData, + responseType: 'json', + unauthenticated: true, + accessKey: accessKey ?? undefined, + groupSendToken: groupSendToken ?? undefined, + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + }); +} + +export async function sendMessages( + destination: ServiceIdString, + messages: ReadonlyArray, + timestamp: number, + { + online, + urgent = true, + story = false, + }: { online?: boolean; story?: boolean; urgent?: boolean } +): Promise { + const jsonData = { + messages, + timestamp, + online: Boolean(online), + urgent, + }; + + await _ajax({ + host: 'chatService', + call: 'messages', + httpType: 'PUT', + urlParameters: `/${destination}?story=${booleanToString(story)}`, + jsonData, + responseType: 'json', + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + }); +} + +function booleanToString(value: boolean | undefined): string { + return value ? 'true' : 'false'; +} + +export async function sendWithSenderKey( + data: Uint8Array, + accessKeys: Uint8Array | null, + groupSendToken: GroupSendToken | null, + timestamp: number, + { + online, + urgent = true, + story = false, + }: { + online?: boolean; + story?: boolean; + urgent?: boolean; + } +): Promise { + const onlineParam = `&online=${booleanToString(online)}`; + const urgentParam = `&urgent=${booleanToString(urgent)}`; + const storyParam = `&story=${booleanToString(story)}`; + + const response = await _ajax({ + host: 'chatService', + call: 'multiRecipient', + httpType: 'PUT', + contentType: 'application/vnd.signal-messenger.mrm', + data, + urlParameters: `?ts=${timestamp}${onlineParam}${urgentParam}${storyParam}`, + responseType: 'json', + unauthenticated: true, + accessKey: accessKeys != null ? Bytes.toBase64(accessKeys) : undefined, + groupSendToken: groupSendToken ?? undefined, + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + }); + const parseResult = safeParseUnknown( + multiRecipient200ResponseSchema, + response + ); + if (parseResult.success) { + return parseResult.data; + } + + log.error( + 'invalid response from sendWithSenderKey', + toLogFormat(parseResult.error) + ); + return response as MultiRecipient200ResponseType; +} + +function redactStickerUrl(stickerUrl: string): string { + return stickerUrl.replace( + /(\/stickers\/)([^/]+)(\/)/, + (_, begin: string, packId: string, end: string) => + `${begin}${redactPackId(packId)}${end}` + ); +} + +export async function getSticker( + packId: string, + stickerId: number +): Promise { + if (!isPackIdValid(packId)) { + throw new Error('getSticker: pack ID was invalid'); + } + return _outerAjax( + `${cdnUrlObject['0']}/stickers/${packId}/full/${stickerId}`, + { + certificateAuthority, + proxyUrl, + responseType: 'bytes', + type: 'GET', + redactUrl: redactStickerUrl, + version, + } + ); +} + +export async function getStickerPackManifest( + packId: string +): Promise { + if (!isPackIdValid(packId)) { + throw new Error('getStickerPackManifest: pack ID was invalid'); + } + return _outerAjax(`${cdnUrlObject['0']}/stickers/${packId}/manifest.proto`, { + certificateAuthority, + proxyUrl, + responseType: 'bytes', + type: 'GET', + redactUrl: redactStickerUrl, + version, + }); +} + +type ServerV2AttachmentType = { + key: string; + credential: string; + acl: string; + algorithm: string; + date: string; + policy: string; + signature: string; +}; + +function makePutParams( + { + key, + credential, + acl, + algorithm, + date, + policy, + signature, + }: ServerV2AttachmentType, + encryptedBin: Uint8Array +) { + // Note: when using the boundary string in the POST body, it needs to be prefixed by + // an extra --, and the final boundary string at the end gets a -- prefix and a -- + // suffix. + const boundaryString = `----------------${getGuid().replace(/-/g, '')}`; + const CRLF = '\r\n'; + const getSection = (name: string, value: string) => + [ + `--${boundaryString}`, + `Content-Disposition: form-data; name="${name}"${CRLF}`, + value, + ].join(CRLF); + + const start = [ + getSection('key', key), + getSection('x-amz-credential', credential), + getSection('acl', acl), + getSection('x-amz-algorithm', algorithm), + getSection('x-amz-date', date), + getSection('policy', policy), + getSection('x-amz-signature', signature), + getSection('Content-Type', 'application/octet-stream'), + `--${boundaryString}`, + 'Content-Disposition: form-data; name="file"', + `Content-Type: application/octet-stream${CRLF}${CRLF}`, + ].join(CRLF); + const end = `${CRLF}--${boundaryString}--${CRLF}`; + + const startBuffer = Bytes.fromString(start); + const attachmentBuffer = encryptedBin; + const endBuffer = Bytes.fromString(end); + + const data = Bytes.concatenate([startBuffer, attachmentBuffer, endBuffer]); + + return { + data, + contentType: `multipart/form-data; boundary=${boundaryString}`, + headers: { + 'Content-Length': data.length.toString(), + }, + }; +} + +export async function putStickers( + encryptedManifest: Uint8Array, + encryptedStickers: ReadonlyArray, + onProgress?: () => void +): Promise { + // Get manifest and sticker upload parameters + const { packId, manifest, stickers } = await _ajax({ + host: 'chatService', + call: 'getStickerPackUpload', + responseType: 'json', + httpType: 'GET', + urlParameters: `/${encryptedStickers.length}`, + zodSchema: StickerPackUploadFormSchema, + }); + + // Upload manifest + const manifestParams = makePutParams(manifest, encryptedManifest); + // This is going to the CDN, not the service, so we use _outerAjax + await _outerAjax(`${cdnUrlObject['0']}/`, { + ...manifestParams, + certificateAuthority, + proxyUrl, + timeout: 0, + type: 'POST', + version, + responseType: 'raw', + }); + + // Upload stickers + const queue = new PQueue({ + concurrency: 3, + timeout: MINUTE * 30, + throwOnTimeout: true, + }); + await Promise.all( + stickers.map(async (sticker: ServerV2AttachmentType, index: number) => { + const stickerParams = makePutParams(sticker, encryptedStickers[index]); + await queue.add(async () => + _outerAjax(`${cdnUrlObject['0']}/`, { + ...stickerParams, certificateAuthority, proxyUrl, timeout: 0, type: 'POST', version, - headers, - redactUrl: () => { - const tmp = new URL(signedUploadLocation); - tmp.search = ''; - tmp.pathname = ''; - return `${tmp}[REDACTED]`; - }, - } + responseType: 'raw', + }) ); + if (onProgress) { + onProgress(); + } + }) + ); - const uploadLocation = uploadResponse.headers.get('location'); + // Done! + return packId; +} + +// Transit tier is the default place for normal (non-backup) attachments. +// Called "transit" because it is transitory +export async function getAttachment({ + cdnKey, + cdnNumber, + options, +}: { + cdnKey: string; + cdnNumber?: number; + options?: { + disableRetries?: boolean; + timeout?: number; + downloadOffset?: number; + abortSignal?: AbortSignal; + }; +}): Promise { + return _getAttachment({ + cdnPath: `/attachments/${cdnKey}`, + cdnNumber: cdnNumber ?? 0, + redactor: _createRedactor(cdnKey), + options, + }); +} + +export async function getAttachmentFromBackupTier({ + mediaId, + backupDir, + mediaDir, + cdnNumber, + headers, + options, +}: GetAttachmentFromBackupTierArgsType): Promise { + return _getAttachment({ + cdnPath: urlPathFromComponents(['backups', backupDir, mediaDir, mediaId]), + cdnNumber, + headers, + redactor: _createRedactor(backupDir, mediaDir, mediaId), + options, + }); +} + +function getCheckedCdnUrl(cdnNumber: number, cdnPath: string) { + const baseUrl = cdnUrlObject[cdnNumber] ?? cdnUrlObject['0']; + const { origin: expectedOrigin } = new URL(baseUrl); + const fullCdnUrl = `${baseUrl}${cdnPath}`; + const { origin } = new URL(fullCdnUrl); + + strictAssert(origin === expectedOrigin, `Unexpected origin: ${origin}`); + return fullCdnUrl; +} + +async function _getAttachmentHeaders({ + cdnPath, + cdnNumber, + headers = {}, + redactor, +}: Omit): Promise { + const fullCdnUrl = getCheckedCdnUrl(cdnNumber, cdnPath); + const response = await _outerAjax(fullCdnUrl, { + headers, + certificateAuthority, + proxyUrl, + responseType: 'raw', + timeout: DEFAULT_TIMEOUT, + type: 'HEAD', + redactUrl: redactor, + version, + }); + return response.headers; +} + +async function _getAttachment({ + cdnPath, + cdnNumber, + headers = {}, + redactor, + options, +}: GetAttachmentArgsType): Promise { + const abortController = new AbortController(); + + let streamWithDetails: StreamWithDetailsType | undefined; + + const cancelRequest = (reason: unknown) => { + abortController.abort(reason); + }; + + options?.abortSignal?.addEventListener('abort', () => + cancelRequest(options.abortSignal?.reason) + ); + + registerInflightRequest(cancelRequest); + + let totalBytes = 0; + + // This is going to the CDN, not the service, so we use _outerAjax + try { + const targetHeaders = { ...headers }; + if (options?.downloadOffset) { + targetHeaders.range = `bytes=${options.downloadOffset}-`; + } + const fullCdnUrl = getCheckedCdnUrl(cdnNumber, cdnPath); + + streamWithDetails = await _outerAjax(fullCdnUrl, { + headers: targetHeaders, + certificateAuthority, + disableRetries: options?.disableRetries, + proxyUrl, + responseType: 'streamwithdetails', + timeout: options?.timeout ?? DEFAULT_TIMEOUT, + type: 'GET', + redactUrl: redactor, + version, + abortSignal: abortController.signal, + }); + + if (targetHeaders.range == null) { + const contentLength = + streamWithDetails.response.headers.get('content-length'); strictAssert( - uploadLocation, - 'attachment upload form header has no location' + contentLength != null, + 'Attachment Content-Length is absent' ); - const redactUrl = () => { - const tmp = new URL(uploadLocation); - tmp.search = ''; - tmp.pathname = ''; - return `${tmp}[REDACTED]`; - }; - - const MAX_RETRIES = 10; - for ( - let start = 0, retries = 0; - start < encryptedSize && retries < MAX_RETRIES; - retries += 1 - ) { - const logId = `putEncryptedAttachment(attempt=${retries})`; - - if (retries !== 0) { - log.warn(`${logId}: resuming from ${start}`); - } - - try { - // This is going to the CDN, not the service, so we use _outerAjax - // eslint-disable-next-line no-await-in-loop - await _outerAjax(uploadLocation, { - disableRetries: true, - certificateAuthority, - proxyUrl, - timeout: 0, - type: 'PUT', - version, - headers: { - 'Content-Range': `bytes ${start}-*/${encryptedSize}`, - }, - data: () => encryptedBin(start), - redactUrl, - responseType: 'raw', - }); - - if (retries !== 0) { - log.warn(`${logId}: Attachment upload succeeded`); - } - return; - } catch (error) { - log.warn( - `${logId}: Failed to upload attachment chunk: ${toLogFormat(error)}` - ); - } - - // eslint-disable-next-line no-await-in-loop - const result: BytesWithDetailsType = await _outerAjax(uploadLocation, { - certificateAuthority, - proxyUrl, - type: 'PUT', - version, - headers: { - 'Content-Range': `bytes */${encryptedSize}`, - }, - data: new Uint8Array(0), - redactUrl, - responseType: 'byteswithdetails', - }); - const { response } = result; - strictAssert(response.status === 308, 'Invalid server response'); - const range = response.headers.get('range'); - if (range != null) { - const match = range.match(/^bytes=0-(\d+)$/); - strictAssert(match != null, `Invalid range header: ${range}`); - start = parseInt(match[1], 10); - } else { - log.warn(`${logId}: No range header`); - } - } - - throw new Error('Upload failed'); - } - - function getHeaderPadding() { - const max = randomInt(1, 64); - let characters = ''; - - for (let i = 0; i < max; i += 1) { - characters += String.fromCharCode(randomInt(65, 122)); - } - - return characters; - } - - async function fetchLinkPreviewMetadata( - href: string, - abortSignal: AbortSignal - ) { - return linkPreviewFetch.fetchLinkPreviewMetadata( - fetchForLinkPreviews, - href, - abortSignal - ); - } - - async function fetchLinkPreviewImage( - href: string, - abortSignal: AbortSignal - ) { - return linkPreviewFetch.fetchLinkPreviewImage( - fetchForLinkPreviews, - href, - abortSignal - ); - } - - async function fetchJsonViaProxy( - params: ProxiedRequestParams - ): Promise { - return _outerAjax(params.url, { - responseType: 'jsonwithdetails', - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - proxyUrl: contentProxyUrl, - type: params.method, - redirect: 'follow', - redactUrl: () => '[REDACTED_URL]', - headers: { - 'X-SignalPadding': getHeaderPadding(), - ...params.headers, - }, - version, - abortSignal: params.signal, - }); - } - - async function fetchBytesViaProxy( - params: ProxiedRequestParams - ): Promise { - return _outerAjax(params.url, { - responseType: 'byteswithdetails', - proxyUrl: contentProxyUrl, - type: params.method, - redirect: 'follow', - redactUrl: () => '[REDACTED_URL]', - headers: { - 'X-SignalPadding': getHeaderPadding(), - ...params.headers, - }, - version, - abortSignal: params.signal, - }); - } - - async function makeSfuRequest( - targetUrl: string, - type: HTTPCodeType, - headers: HeaderListType, - body: Uint8Array | undefined - ): Promise { - return _outerAjax(targetUrl, { - certificateAuthority, - data: body, - headers, - proxyUrl, - responseType: 'byteswithdetails', - timeout: 0, - type, - version, - }); - } - - // Groups - - function generateGroupAuth( - groupPublicParamsHex: string, - authCredentialPresentationHex: string - ) { - return Bytes.toBase64( - Bytes.fromString( - `${groupPublicParamsHex}:${authCredentialPresentationHex}` - ) - ); - } - - async function getGroupCredentials({ - startDayInMs, - endDayInMs, - }: GetGroupCredentialsOptionsType): Promise { - const startDayInSeconds = startDayInMs / SECOND; - const endDayInSeconds = endDayInMs / SECOND; - - const response = await _ajax({ - host: 'chatService', - call: 'getGroupCredentials', - urlParameters: - `?redemptionStartSeconds=${startDayInSeconds}&` + - `redemptionEndSeconds=${endDayInSeconds}&` + - 'zkcCredential=true', - httpType: 'GET', - responseType: 'json', - // TODO DESKTOP-8719 - zodSchema: z.unknown(), - }); - - return response as GetGroupCredentialsResultType; - } - - async function getExternalGroupCredential( - options: GroupCredentialsType - ): Promise { - const basicAuth = generateGroupAuth( - options.groupPublicParamsHex, - options.authCredentialPresentationHex + const maybeSize = safeParseNumber(contentLength); + strictAssert( + maybeSize != null, + 'Attachment Content-Length is not a number' ); - const response = await _ajax({ - basicAuth, - call: 'groupToken', - httpType: 'GET', - contentType: 'application/x-protobuf', - responseType: 'bytes', - host: 'storageService', - disableSessionResumption: true, - }); - - return Proto.ExternalGroupCredential.decode(response); - } - - function verifyAttributes(attributes: Proto.IAvatarUploadAttributes) { - const { key, credential, acl, algorithm, date, policy, signature } = - attributes; - - if ( - !key || - !credential || - !acl || - !algorithm || - !date || - !policy || - !signature - ) { - throw new Error( - 'verifyAttributes: Missing value from AvatarUploadAttributes' - ); - } - - return { - key, - credential, - acl, - algorithm, - date, - policy, - signature, - }; - } - - async function uploadAvatar( - uploadAvatarRequestHeaders: UploadAvatarHeadersType, - avatarData: Uint8Array - ): Promise { - const verified = verifyAttributes(uploadAvatarRequestHeaders); - const { key } = verified; - - const manifestParams = makePutParams(verified, avatarData); - - await _outerAjax(`${cdnUrlObject['0']}/`, { - ...manifestParams, - certificateAuthority, - proxyUrl, - timeout: 0, - type: 'POST', - version, - responseType: 'raw', - }); - - return key; - } - - async function uploadGroupAvatar( - avatarData: Uint8Array, - options: GroupCredentialsType - ): Promise { - const basicAuth = generateGroupAuth( - options.groupPublicParamsHex, - options.authCredentialPresentationHex + totalBytes = maybeSize; + } else { + strictAssert( + streamWithDetails.response.status === 206, + `Expected 206 status code for offset ${options?.downloadOffset}` + ); + strictAssert( + !streamWithDetails.contentType?.includes('multipart'), + 'Expected non-multipart response' ); - const response = await _ajax({ - basicAuth, - call: 'getGroupAvatarUpload', - httpType: 'GET', - responseType: 'bytes', - host: 'storageService', - disableSessionResumption: true, - }); - const attributes = Proto.AvatarUploadAttributes.decode(response); + const range = streamWithDetails.response.headers.get('content-range'); + strictAssert(range != null, 'Attachment Content-Range is absent'); - const verified = verifyAttributes(attributes); - const { key } = verified; - - const manifestParams = makePutParams(verified, avatarData); - - await _outerAjax(`${cdnUrlObject['0']}/`, { - ...manifestParams, - certificateAuthority, - proxyUrl, - timeout: 0, - type: 'POST', - version, - responseType: 'raw', - }); - - return key; - } - - async function getGroupAvatar(key: string): Promise { - return _outerAjax(`${cdnUrlObject['0']}/${key}`, { - certificateAuthority, - proxyUrl, - responseType: 'bytes', - timeout: 0, - type: 'GET', - version, - redactUrl: _createRedactor(key), - }); - } - - function createBoostPaymentIntent( - options: CreateBoostOptionsType - ): Promise { - return _ajax({ - unauthenticated: true, - host: 'chatService', - call: 'createBoost', - httpType: 'POST', - jsonData: options, - responseType: 'json', - zodSchema: CreateBoostResultSchema, - }); - } - - async function createBoostReceiptCredentials( - options: CreateBoostReceiptCredentialsOptionsType - ): Promise> { - return _ajax({ - unauthenticated: true, - host: 'chatService', - call: 'boostReceiptCredentials', - httpType: 'POST', - jsonData: options, - responseType: 'jsonwithdetails', - zodSchema: CreateBoostReceiptCredentialsResultSchema, - }); - } - - // https://docs.stripe.com/api/payment_intents/confirm?api-version=2025-05-28.basil - function confirmIntentWithStripe( - options: ConfirmIntentWithStripeOptionsType - ): Promise { - const { - clientSecret, - idempotencyKey, - paymentIntentId, - paymentMethodId, - returnUrl, - } = options; - const safePaymentIntentId = encodeURIComponent(paymentIntentId); - const url = `https://api.stripe.com/v1/payment_intents/${safePaymentIntentId}/confirm`; - const formData = { - client_secret: clientSecret, - payment_method: paymentMethodId, - return_url: returnUrl, - }; - const basicAuth = getBasicAuth({ - username: stripePublishableKey, - password: '', - }); - const formBytes = Bytes.fromString(qs.encode(formData)); - - // This is going to Stripe, so we use _outerAjax - return _outerAjax(url, { - data: formBytes, - headers: { - Authorization: basicAuth, - 'Content-Type': CONTENT_TYPE_FORM_ENCODING, - 'Content-Length': formBytes.byteLength.toString(), - 'Idempotency-Key': idempotencyKey, - }, - proxyUrl, - redactUrl: () => { - return url.replace(safePaymentIntentId, '[REDACTED]'); - }, - responseType: 'json', - type: 'POST', - version, - zodSchema: ConfirmIntentWithStripeResultSchema, - }); - } - - // https://docs.stripe.com/api/payment_methods/create?api-version=2025-05-28.basil&lang=node#create_payment_method-card - function createPaymentMethodWithStripe( - options: CreatePaymentMethodWithStripeOptionsType - ): Promise { - const { cardDetail } = options; - const formData = { - type: 'card', - 'card[cvc]': cardDetail.cvc, - 'card[exp_month]': cardDetail.expirationMonth, - 'card[exp_year]': cardDetail.expirationYear, - 'card[number]': cardDetail.number, - }; - const basicAuth = getBasicAuth({ - username: stripePublishableKey, - password: '', - }); - const formBytes = Bytes.fromString(qs.encode(formData)); - - // This is going to Stripe, so we use _outerAjax - return _outerAjax('https://api.stripe.com/v1/payment_methods', { - data: formBytes, - headers: { - Authorization: basicAuth, - 'Content-Type': CONTENT_TYPE_FORM_ENCODING, - 'Content-Length': formBytes.byteLength.toString(), - }, - proxyUrl, - responseType: 'json', - type: 'POST', - version, - zodSchema: CreatePaymentMethodWithStripeResultSchema, - }); - } - - async function createGroup( - group: Proto.IGroup, - options: GroupCredentialsType - ): Promise { - const basicAuth = generateGroupAuth( - options.groupPublicParamsHex, - options.authCredentialPresentationHex - ); - const data = Proto.Group.encode(group).finish(); - - const response = await _ajax({ - basicAuth, - call: 'groups', - contentType: 'application/x-protobuf', - data, - host: 'storageService', - disableSessionResumption: true, - httpType: 'PUT', - responseType: 'bytes', - }); - - return Proto.GroupResponse.decode(response); - } - - async function getGroup( - options: GroupCredentialsType - ): Promise { - const basicAuth = generateGroupAuth( - options.groupPublicParamsHex, - options.authCredentialPresentationHex + const match = PARSE_RANGE_HEADER.exec(range); + strictAssert(match != null, 'Attachment Content-Range is invalid'); + const maybeSize = safeParseNumber(match[1]); + strictAssert( + maybeSize != null, + 'Attachment Content-Range[1] is not a number' ); - const response = await _ajax({ - basicAuth, - call: 'groups', - contentType: 'application/x-protobuf', - host: 'storageService', - disableSessionResumption: true, - httpType: 'GET', - responseType: 'bytes', - }); - - return Proto.GroupResponse.decode(response); + totalBytes = maybeSize; } - - async function getGroupFromLink( - inviteLinkPassword: string | undefined, - auth: GroupCredentialsType - ): Promise { - const basicAuth = generateGroupAuth( - auth.groupPublicParamsHex, - auth.authCredentialPresentationHex - ); - const safeInviteLinkPassword = inviteLinkPassword - ? toWebSafeBase64(inviteLinkPassword) - : undefined; - - const response = await _ajax({ - basicAuth, - call: 'groupsViaLink', - contentType: 'application/x-protobuf', - host: 'storageService', - disableSessionResumption: true, - httpType: 'GET', - responseType: 'bytes', - urlParameters: safeInviteLinkPassword - ? `${safeInviteLinkPassword}` - : undefined, - redactUrl: _createRedactor(safeInviteLinkPassword), - }); - - return Proto.GroupJoinInfo.decode(response); - } - - async function modifyGroup( - changes: Proto.GroupChange.IActions, - options: GroupCredentialsType, - inviteLinkBase64?: string - ): Promise { - const basicAuth = generateGroupAuth( - options.groupPublicParamsHex, - options.authCredentialPresentationHex - ); - const data = Proto.GroupChange.Actions.encode(changes).finish(); - const safeInviteLinkPassword = inviteLinkBase64 - ? toWebSafeBase64(inviteLinkBase64) - : undefined; - - const response = await _ajax({ - basicAuth, - call: 'groups', - contentType: 'application/x-protobuf', - data, - host: 'storageService', - disableSessionResumption: true, - httpType: 'PATCH', - responseType: 'bytes', - urlParameters: safeInviteLinkPassword - ? `?inviteLinkPassword=${safeInviteLinkPassword}` - : undefined, - redactUrl: safeInviteLinkPassword - ? _createRedactor(safeInviteLinkPassword) - : undefined, - }); - - return Proto.GroupChangeResponse.decode(response); - } - - async function getGroupLog( - options: GetGroupLogOptionsType, - credentials: GroupCredentialsType - ): Promise { - const basicAuth = generateGroupAuth( - credentials.groupPublicParamsHex, - credentials.authCredentialPresentationHex - ); - - const { - startVersion, - includeFirstState, - includeLastState, - maxSupportedChangeEpoch, - cachedEndorsementsExpiration, - } = options; - - // If we don't know starting revision - fetch it from the server - if (startVersion === undefined) { - const { data: joinedData } = await _ajax({ - basicAuth, - call: 'groupJoinedAtVersion', - contentType: 'application/x-protobuf', - host: 'storageService', - disableSessionResumption: true, - httpType: 'GET', - responseType: 'byteswithdetails', - }); - - const { joinedAtVersion } = Proto.Member.decode(joinedData); - - return getGroupLog( - { - ...options, - startVersion: joinedAtVersion ?? 0, - }, - credentials - ); - } - - const withDetails = await _ajax({ - basicAuth, - call: 'groupLog', - contentType: 'application/x-protobuf', - host: 'storageService', - disableSessionResumption: true, - httpType: 'GET', - responseType: 'byteswithdetails', - headers: { - 'Cached-Send-Endorsements': String(cachedEndorsementsExpiration ?? 0), - }, - urlParameters: - `/${startVersion}?` + - `includeFirstState=${Boolean(includeFirstState)}&` + - `includeLastState=${Boolean(includeLastState)}&` + - `maxSupportedChangeEpoch=${Number(maxSupportedChangeEpoch)}`, - }); - const { data, response } = withDetails; - const changes = Proto.GroupChanges.decode(data); - const { groupSendEndorsementResponse } = changes; - - if (response && response.status === 206) { - const range = response.headers.get('Content-Range'); - const match = PARSE_GROUP_LOG_RANGE_HEADER.exec(range || ''); - - const start = match ? parseInt(match[1], 10) : undefined; - const end = match ? parseInt(match[2], 10) : undefined; - const currentRevision = match ? parseInt(match[3], 10) : undefined; - - if ( - match && - isNumber(start) && - isNumber(end) && - isNumber(currentRevision) - ) { - return { - paginated: true, - changes, - start, - end, - currentRevision, - groupSendEndorsementResponse, - }; - } - } - - return { - paginated: false, - changes, - groupSendEndorsementResponse, - }; - } - - async function getSubscription( - subscriberId: Uint8Array - ): Promise { - const formattedId = toWebSafeBase64(Bytes.toBase64(subscriberId)); - return _ajax({ - host: 'chatService', - call: 'subscriptions', - httpType: 'GET', - urlParameters: `/${formattedId}`, - responseType: 'json', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - redactUrl: _createRedactor(formattedId), - zodSchema: subscriptionResponseSchema, - }); - } - - async function getHasSubscription( - subscriberId: Uint8Array - ): Promise { - const data = await getSubscription(subscriberId); - if (!data.subscription) { - return false; - } - return data.subscription.active; - } - - function getProvisioningResource( - handler: IRequestHandler, - timeout?: number - ): Promise { - return socketManager.getProvisioningResource(handler, timeout); - } - - async function cdsLookup({ - e164s, - acisAndAccessKeys = [], - returnAcisWithoutUaks, - }: CdsLookupOptionsType): Promise { - return cds.request({ - e164s, - acisAndAccessKeys, - returnAcisWithoutUaks, + } finally { + if (!streamWithDetails) { + unregisterInFlightRequest(cancelRequest); + } else { + streamWithDetails.stream.on('close', () => { + unregisterInFlightRequest(cancelRequest); }); } } + + const timeoutStream = getTimeoutStream({ + name: `getAttachment(${redactor(cdnPath)})`, + timeout: GET_ATTACHMENT_CHUNK_TIMEOUT, + abortController, + }); + + const combinedStream = streamWithDetails.stream + // We do this manually; pipe() doesn't flow errors through the streams for us + .on('error', (error: Error) => { + timeoutStream.emit('error', error); + }) + .pipe(timeoutStream); + + if (options?.onProgress) { + const { onProgress } = options; + let currentBytes = options.downloadOffset ?? 0; + + combinedStream.pause(); + combinedStream.on('data', chunk => { + currentBytes += chunk.byteLength; + onProgress(currentBytes, totalBytes); + }); + onProgress(0, totalBytes); + } + + return combinedStream; +} + +export async function getAttachmentUploadForm(): Promise { + return _ajax({ + host: 'chatService', + call: 'attachmentUploadForm', + httpType: 'GET', + responseType: 'json', + zodSchema: attachmentUploadFormResponse, + }); +} + +export async function putEncryptedAttachment( + encryptedBin: (start: number, end?: number) => Readable, + encryptedSize: number, + uploadForm: AttachmentUploadFormResponseType +): Promise { + const { signedUploadLocation, headers } = uploadForm; + + // This is going to the CDN, not the service, so we use _outerAjax + const { response: uploadResponse } = await _outerAjax(signedUploadLocation, { + responseType: 'byteswithdetails', + certificateAuthority, + proxyUrl, + timeout: 0, + type: 'POST', + version, + headers, + redactUrl: () => { + const tmp = new URL(signedUploadLocation); + tmp.search = ''; + tmp.pathname = ''; + return `${tmp}[REDACTED]`; + }, + }); + + const uploadLocation = uploadResponse.headers.get('location'); + strictAssert(uploadLocation, 'attachment upload form header has no location'); + + const redactUrl = () => { + const tmp = new URL(uploadLocation); + tmp.search = ''; + tmp.pathname = ''; + return `${tmp}[REDACTED]`; + }; + + const MAX_RETRIES = 10; + for ( + let start = 0, retries = 0; + start < encryptedSize && retries < MAX_RETRIES; + retries += 1 + ) { + const logId = `putEncryptedAttachment(attempt=${retries})`; + + if (retries !== 0) { + log.warn(`${logId}: resuming from ${start}`); + } + + try { + // This is going to the CDN, not the service, so we use _outerAjax + // eslint-disable-next-line no-await-in-loop + await _outerAjax(uploadLocation, { + disableRetries: true, + certificateAuthority, + proxyUrl, + timeout: 0, + type: 'PUT', + version, + headers: { + 'Content-Range': `bytes ${start}-*/${encryptedSize}`, + }, + data: () => encryptedBin(start), + redactUrl, + responseType: 'raw', + }); + + if (retries !== 0) { + log.warn(`${logId}: Attachment upload succeeded`); + } + return; + } catch (error) { + log.warn( + `${logId}: Failed to upload attachment chunk: ${toLogFormat(error)}` + ); + } + + // eslint-disable-next-line no-await-in-loop + const result: BytesWithDetailsType = await _outerAjax(uploadLocation, { + certificateAuthority, + proxyUrl, + type: 'PUT', + version, + headers: { + 'Content-Range': `bytes */${encryptedSize}`, + }, + data: new Uint8Array(0), + redactUrl, + responseType: 'byteswithdetails', + }); + const { response } = result; + strictAssert(response.status === 308, 'Invalid server response'); + const range = response.headers.get('range'); + if (range != null) { + const match = range.match(/^bytes=0-(\d+)$/); + strictAssert(match != null, `Invalid range header: ${range}`); + start = parseInt(match[1], 10); + } else { + log.warn(`${logId}: No range header`); + } + } + + throw new Error('Upload failed'); +} + +function getHeaderPadding() { + const max = randomInt(1, 64); + let characters = ''; + + for (let i = 0; i < max; i += 1) { + characters += String.fromCharCode(randomInt(65, 122)); + } + + return characters; +} + +export async function fetchLinkPreviewMetadata( + href: string, + abortSignal: AbortSignal +): Promise { + return linkPreviewFetch.fetchLinkPreviewMetadata( + fetchForLinkPreviews, + href, + abortSignal + ); +} + +export async function fetchLinkPreviewImage( + href: string, + abortSignal: AbortSignal +): Promise { + return linkPreviewFetch.fetchLinkPreviewImage( + fetchForLinkPreviews, + href, + abortSignal + ); +} + +export async function fetchJsonViaProxy( + params: ProxiedRequestParams +): Promise { + return _outerAjax(params.url, { + responseType: 'jsonwithdetails', + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + proxyUrl: contentProxyUrl, + type: params.method, + redirect: 'follow', + redactUrl: () => '[REDACTED_URL]', + headers: { + 'X-SignalPadding': getHeaderPadding(), + ...params.headers, + }, + version, + abortSignal: params.signal, + }); +} + +export async function fetchBytesViaProxy( + params: ProxiedRequestParams +): Promise { + return _outerAjax(params.url, { + responseType: 'byteswithdetails', + proxyUrl: contentProxyUrl, + type: params.method, + redirect: 'follow', + redactUrl: () => '[REDACTED_URL]', + headers: { + 'X-SignalPadding': getHeaderPadding(), + ...params.headers, + }, + version, + abortSignal: params.signal, + }); +} + +export async function makeSfuRequest( + targetUrl: string, + type: HTTPCodeType, + headers: HeaderListType, + body: Uint8Array | undefined +): Promise { + return _outerAjax(targetUrl, { + certificateAuthority, + data: body, + headers, + proxyUrl, + responseType: 'byteswithdetails', + timeout: 0, + type, + version, + }); +} + +// Groups + +function generateGroupAuth( + groupPublicParamsHex: string, + authCredentialPresentationHex: string +) { + return Bytes.toBase64( + Bytes.fromString(`${groupPublicParamsHex}:${authCredentialPresentationHex}`) + ); +} + +export async function getGroupCredentials({ + startDayInMs, + endDayInMs, +}: GetGroupCredentialsOptionsType): Promise { + const startDayInSeconds = startDayInMs / SECOND; + const endDayInSeconds = endDayInMs / SECOND; + + const response = await _ajax({ + host: 'chatService', + call: 'getGroupCredentials', + urlParameters: + `?redemptionStartSeconds=${startDayInSeconds}&` + + `redemptionEndSeconds=${endDayInSeconds}&` + + 'zkcCredential=true', + httpType: 'GET', + responseType: 'json', + // TODO DESKTOP-8719 + zodSchema: z.unknown(), + }); + + return response as GetGroupCredentialsResultType; +} + +export async function getExternalGroupCredential( + options: GroupCredentialsType +): Promise { + const basicAuth = generateGroupAuth( + options.groupPublicParamsHex, + options.authCredentialPresentationHex + ); + + const response = await _ajax({ + basicAuth, + call: 'groupToken', + httpType: 'GET', + contentType: 'application/x-protobuf', + responseType: 'bytes', + host: 'storageService', + disableSessionResumption: true, + }); + + return Proto.ExternalGroupCredential.decode(response); +} + +function verifyAttributes(attributes: Proto.IAvatarUploadAttributes) { + const { key, credential, acl, algorithm, date, policy, signature } = + attributes; + + if ( + !key || + !credential || + !acl || + !algorithm || + !date || + !policy || + !signature + ) { + throw new Error( + 'verifyAttributes: Missing value from AvatarUploadAttributes' + ); + } + + return { + key, + credential, + acl, + algorithm, + date, + policy, + signature, + }; +} + +export async function uploadAvatar( + uploadAvatarRequestHeaders: UploadAvatarHeadersType, + avatarData: Uint8Array +): Promise { + const verified = verifyAttributes(uploadAvatarRequestHeaders); + const { key } = verified; + + const manifestParams = makePutParams(verified, avatarData); + + await _outerAjax(`${cdnUrlObject['0']}/`, { + ...manifestParams, + certificateAuthority, + proxyUrl, + timeout: 0, + type: 'POST', + version, + responseType: 'raw', + }); + + return key; +} + +export async function uploadGroupAvatar( + avatarData: Uint8Array, + options: GroupCredentialsType +): Promise { + const basicAuth = generateGroupAuth( + options.groupPublicParamsHex, + options.authCredentialPresentationHex + ); + + const response = await _ajax({ + basicAuth, + call: 'getGroupAvatarUpload', + httpType: 'GET', + responseType: 'bytes', + host: 'storageService', + disableSessionResumption: true, + }); + const attributes = Proto.AvatarUploadAttributes.decode(response); + + const verified = verifyAttributes(attributes); + const { key } = verified; + + const manifestParams = makePutParams(verified, avatarData); + + await _outerAjax(`${cdnUrlObject['0']}/`, { + ...manifestParams, + certificateAuthority, + proxyUrl, + timeout: 0, + type: 'POST', + version, + responseType: 'raw', + }); + + return key; +} + +export async function getGroupAvatar(key: string): Promise { + return _outerAjax(`${cdnUrlObject['0']}/${key}`, { + certificateAuthority, + proxyUrl, + responseType: 'bytes', + timeout: 0, + type: 'GET', + version, + redactUrl: _createRedactor(key), + }); +} + +export function createBoostPaymentIntent( + options: CreateBoostOptionsType +): Promise { + return _ajax({ + unauthenticated: true, + host: 'chatService', + call: 'createBoost', + httpType: 'POST', + jsonData: options, + responseType: 'json', + zodSchema: CreateBoostResultSchema, + }); +} + +export async function createBoostReceiptCredentials( + options: CreateBoostReceiptCredentialsOptionsType +): Promise> { + return _ajax({ + unauthenticated: true, + host: 'chatService', + call: 'boostReceiptCredentials', + httpType: 'POST', + jsonData: options, + responseType: 'jsonwithdetails', + zodSchema: CreateBoostReceiptCredentialsResultSchema, + }); +} + +// https://docs.stripe.com/api/payment_intents/confirm?api-version=2025-05-28.basil +export function confirmIntentWithStripe( + options: ConfirmIntentWithStripeOptionsType +): Promise { + const { + clientSecret, + idempotencyKey, + paymentIntentId, + paymentMethodId, + returnUrl, + } = options; + const safePaymentIntentId = encodeURIComponent(paymentIntentId); + const url = `https://api.stripe.com/v1/payment_intents/${safePaymentIntentId}/confirm`; + const formData = { + client_secret: clientSecret, + payment_method: paymentMethodId, + return_url: returnUrl, + }; + const basicAuth = getBasicAuth({ + username: stripePublishableKey, + password: '', + }); + const formBytes = Bytes.fromString(qs.encode(formData)); + + // This is going to Stripe, so we use _outerAjax + return _outerAjax(url, { + data: formBytes, + headers: { + Authorization: basicAuth, + 'Content-Type': CONTENT_TYPE_FORM_ENCODING, + 'Content-Length': formBytes.byteLength.toString(), + 'Idempotency-Key': idempotencyKey, + }, + proxyUrl, + redactUrl: () => { + return url.replace(safePaymentIntentId, '[REDACTED]'); + }, + responseType: 'json', + type: 'POST', + version, + zodSchema: ConfirmIntentWithStripeResultSchema, + }); +} + +// https://docs.stripe.com/api/payment_methods/create?api-version=2025-05-28.basil&lang=node#create_payment_method-card +export function createPaymentMethodWithStripe( + options: CreatePaymentMethodWithStripeOptionsType +): Promise { + const { cardDetail } = options; + const formData = { + type: 'card', + 'card[cvc]': cardDetail.cvc, + 'card[exp_month]': cardDetail.expirationMonth, + 'card[exp_year]': cardDetail.expirationYear, + 'card[number]': cardDetail.number, + }; + const basicAuth = getBasicAuth({ + username: stripePublishableKey, + password: '', + }); + const formBytes = Bytes.fromString(qs.encode(formData)); + + // This is going to Stripe, so we use _outerAjax + return _outerAjax('https://api.stripe.com/v1/payment_methods', { + data: formBytes, + headers: { + Authorization: basicAuth, + 'Content-Type': CONTENT_TYPE_FORM_ENCODING, + 'Content-Length': formBytes.byteLength.toString(), + }, + proxyUrl, + responseType: 'json', + type: 'POST', + version, + zodSchema: CreatePaymentMethodWithStripeResultSchema, + }); +} + +export async function createGroup( + group: Proto.IGroup, + options: GroupCredentialsType +): Promise { + const basicAuth = generateGroupAuth( + options.groupPublicParamsHex, + options.authCredentialPresentationHex + ); + const data = Proto.Group.encode(group).finish(); + + const response = await _ajax({ + basicAuth, + call: 'groups', + contentType: 'application/x-protobuf', + data, + host: 'storageService', + disableSessionResumption: true, + httpType: 'PUT', + responseType: 'bytes', + }); + + return Proto.GroupResponse.decode(response); +} + +export async function getGroup( + options: GroupCredentialsType +): Promise { + const basicAuth = generateGroupAuth( + options.groupPublicParamsHex, + options.authCredentialPresentationHex + ); + + const response = await _ajax({ + basicAuth, + call: 'groups', + contentType: 'application/x-protobuf', + host: 'storageService', + disableSessionResumption: true, + httpType: 'GET', + responseType: 'bytes', + }); + + return Proto.GroupResponse.decode(response); +} + +export async function getGroupFromLink( + inviteLinkPassword: string | undefined, + auth: GroupCredentialsType +): Promise { + const basicAuth = generateGroupAuth( + auth.groupPublicParamsHex, + auth.authCredentialPresentationHex + ); + const safeInviteLinkPassword = inviteLinkPassword + ? toWebSafeBase64(inviteLinkPassword) + : undefined; + + const response = await _ajax({ + basicAuth, + call: 'groupsViaLink', + contentType: 'application/x-protobuf', + host: 'storageService', + disableSessionResumption: true, + httpType: 'GET', + responseType: 'bytes', + urlParameters: safeInviteLinkPassword + ? `${safeInviteLinkPassword}` + : undefined, + redactUrl: _createRedactor(safeInviteLinkPassword), + }); + + return Proto.GroupJoinInfo.decode(response); +} + +export async function modifyGroup( + changes: Proto.GroupChange.IActions, + options: GroupCredentialsType, + inviteLinkBase64?: string +): Promise { + const basicAuth = generateGroupAuth( + options.groupPublicParamsHex, + options.authCredentialPresentationHex + ); + const data = Proto.GroupChange.Actions.encode(changes).finish(); + const safeInviteLinkPassword = inviteLinkBase64 + ? toWebSafeBase64(inviteLinkBase64) + : undefined; + + const response = await _ajax({ + basicAuth, + call: 'groups', + contentType: 'application/x-protobuf', + data, + host: 'storageService', + disableSessionResumption: true, + httpType: 'PATCH', + responseType: 'bytes', + urlParameters: safeInviteLinkPassword + ? `?inviteLinkPassword=${safeInviteLinkPassword}` + : undefined, + redactUrl: safeInviteLinkPassword + ? _createRedactor(safeInviteLinkPassword) + : undefined, + }); + + return Proto.GroupChangeResponse.decode(response); +} + +export async function getGroupLog( + options: GetGroupLogOptionsType, + credentials: GroupCredentialsType +): Promise { + const basicAuth = generateGroupAuth( + credentials.groupPublicParamsHex, + credentials.authCredentialPresentationHex + ); + + const { + startVersion, + includeFirstState, + includeLastState, + maxSupportedChangeEpoch, + cachedEndorsementsExpiration, + } = options; + + // If we don't know starting revision - fetch it from the server + if (startVersion === undefined) { + const { data: joinedData } = await _ajax({ + basicAuth, + call: 'groupJoinedAtVersion', + contentType: 'application/x-protobuf', + host: 'storageService', + disableSessionResumption: true, + httpType: 'GET', + responseType: 'byteswithdetails', + }); + + const { joinedAtVersion } = Proto.Member.decode(joinedData); + + return getGroupLog( + { + ...options, + startVersion: joinedAtVersion ?? 0, + }, + credentials + ); + } + + const withDetails = await _ajax({ + basicAuth, + call: 'groupLog', + contentType: 'application/x-protobuf', + host: 'storageService', + disableSessionResumption: true, + httpType: 'GET', + responseType: 'byteswithdetails', + headers: { + 'Cached-Send-Endorsements': String(cachedEndorsementsExpiration ?? 0), + }, + urlParameters: + `/${startVersion}?` + + `includeFirstState=${Boolean(includeFirstState)}&` + + `includeLastState=${Boolean(includeLastState)}&` + + `maxSupportedChangeEpoch=${Number(maxSupportedChangeEpoch)}`, + }); + const { data, response } = withDetails; + const changes = Proto.GroupChanges.decode(data); + const { groupSendEndorsementResponse } = changes; + + if (response && response.status === 206) { + const range = response.headers.get('Content-Range'); + const match = PARSE_GROUP_LOG_RANGE_HEADER.exec(range || ''); + + const start = match ? parseInt(match[1], 10) : undefined; + const end = match ? parseInt(match[2], 10) : undefined; + const currentRevision = match ? parseInt(match[3], 10) : undefined; + + if ( + match && + isNumber(start) && + isNumber(end) && + isNumber(currentRevision) + ) { + return { + paginated: true, + changes, + start, + end, + currentRevision, + groupSendEndorsementResponse, + }; + } + } + + return { + paginated: false, + changes, + groupSendEndorsementResponse, + }; +} + +export async function getSubscription( + subscriberId: Uint8Array +): Promise { + const formattedId = toWebSafeBase64(Bytes.toBase64(subscriberId)); + return _ajax({ + host: 'chatService', + call: 'subscriptions', + httpType: 'GET', + urlParameters: `/${formattedId}`, + responseType: 'json', + unauthenticated: true, + accessKey: undefined, + groupSendToken: undefined, + redactUrl: _createRedactor(formattedId), + zodSchema: subscriptionResponseSchema, + }); +} + +export async function getHasSubscription( + subscriberId: Uint8Array +): Promise { + const data = await getSubscription(subscriberId); + if (!data.subscription) { + return false; + } + return data.subscription.active; +} + +export function getProvisioningResource( + handler: IRequestHandler, + timeout?: number +): Promise { + return socketManager.getProvisioningResource(handler, timeout); +} + +export async function cdsLookup({ + e164s, + acisAndAccessKeys = [], + returnAcisWithoutUaks, +}: CdsLookupOptionsType): Promise { + return cds.request({ + e164s, + acisAndAccessKeys, + returnAcisWithoutUaks, + }); } // TODO: DESKTOP-8300 diff --git a/ts/textsecure/WebsocketResources.ts b/ts/textsecure/WebsocketResources.ts index 2479e4e02c..8ffc126caa 100644 --- a/ts/textsecure/WebsocketResources.ts +++ b/ts/textsecure/WebsocketResources.ts @@ -59,10 +59,8 @@ import { AbortableProcess } from '../util/AbortableProcess.js'; import type { WebAPICredentials } from './Types.js'; import { NORMAL_DISCONNECT_CODE } from './SocketManager.js'; import { parseUnknown } from '../util/schemas.js'; -import { - parseServerAlertsFromHeader, - type ServerAlert, -} from '../util/handleServerAlerts.js'; +import { parseServerAlertsFromHeader } from '../util/handleServerAlerts.js'; +import type { ServerAlert } from '../types/ServerAlert.js'; const log = createLogger('WebsocketResources'); diff --git a/ts/textsecure/downloadAttachment.ts b/ts/textsecure/downloadAttachment.ts index 1197ac0183..dc6c09a860 100644 --- a/ts/textsecure/downloadAttachment.ts +++ b/ts/textsecure/downloadAttachment.ts @@ -30,7 +30,7 @@ import { type IntegrityCheckType, } from '../AttachmentCrypto.js'; import type { ProcessedAttachment } from './Types.d.ts'; -import type { WebAPIType } from './WebAPI.js'; +import type { getAttachment, getAttachmentFromBackupTier } from './WebAPI.js'; import { getAttachmentCiphertextSize } from '../util/AttachmentCrypto.js'; import { createName, getRelativePath } from '../util/attachmentPath.js'; import { MediaTier } from '../types/AttachmentDownload.js'; @@ -108,8 +108,13 @@ export async function getCdnNumberForBackupTier( return backupCdnNumber; } +type ServerType = Readonly<{ + getAttachment: typeof getAttachment; + getAttachmentFromBackupTier: typeof getAttachmentFromBackupTier; +}>; + export async function downloadAttachment( - server: WebAPIType, + server: ServerType, { attachment, mediaTier, diff --git a/ts/textsecure/getKeysForServiceId.ts b/ts/textsecure/getKeysForServiceId.ts index 59042b46cb..719557a748 100644 --- a/ts/textsecure/getKeysForServiceId.ts +++ b/ts/textsecure/getKeysForServiceId.ts @@ -16,19 +16,29 @@ import { Sessions, IdentityKeys } from '../LibSignalStores.js'; import { Address } from '../types/Address.js'; import { QualifiedAddress } from '../types/QualifiedAddress.js'; import type { ServiceIdString } from '../types/ServiceId.js'; -import type { ServerKeysType, WebAPIType } from './WebAPI.js'; +import type { + getKeysForServiceId as doGetKeysForServiceId, + getKeysForServiceIdUnauth, + ServerKeysType, +} from './WebAPI.js'; import { createLogger } from '../logging/log.js'; import { isRecord } from '../util/isRecord.js'; import type { GroupSendToken } from '../types/GroupSendEndorsements.js'; import { HTTPError } from '../types/HTTPError.js'; import { onFailedToSendWithEndorsements } from '../util/groupSendEndorsements.js'; import { signalProtocolStore } from '../SignalProtocolStore.js'; +import { itemStorage } from './Storage.js'; const log = createLogger('getKeysForServiceId'); +type ServerType = Readonly<{ + getKeysForServiceId: typeof doGetKeysForServiceId; + getKeysForServiceIdUnauth: typeof getKeysForServiceIdUnauth; +}>; + export async function getKeysForServiceId( serviceId: ServiceIdString, - server: WebAPIType, + server: ServerType, devicesToUpdate: Array | null, accessKey: string | null, groupSendToken: GroupSendToken | null @@ -67,7 +77,7 @@ function isUnauthorizedError(error: unknown) { async function getServerKeys( serviceId: ServiceIdString, - server: WebAPIType, + server: ServerType, accessKey: string | null, groupSendToken: GroupSendToken | null ): Promise<{ accessKeyFailed: boolean; keys: ServerKeysType }> { @@ -119,7 +129,7 @@ async function handleServerKeys( response: ServerKeysType, devicesToUpdate: Array | null ): Promise { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const sessionStore = new Sessions({ ourServiceId: ourAci }); const identityKeyStore = new IdentityKeys({ ourServiceId: ourAci }); diff --git a/ts/textsecure/index.ts b/ts/textsecure/index.ts deleted file mode 100644 index c01ac8306e..0000000000 --- a/ts/textsecure/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import EventTarget from './EventTarget.js'; -import AccountManager from './AccountManager.js'; -import MessageReceiver from './MessageReceiver.js'; -import utils from './Helpers.js'; -import MessageSender from './SendMessage.js'; -import { Storage } from './Storage.js'; -import * as WebAPI from './WebAPI.js'; -import WebSocketResource from './WebsocketResources.js'; - -export type TextSecureType = { - utils: typeof utils; - storage: Storage; - - AccountManager: typeof AccountManager; - EventTarget: typeof EventTarget; - MessageReceiver: typeof MessageReceiver; - MessageSender: typeof MessageSender; - WebAPI: typeof WebAPI; - WebSocketResource: typeof WebSocketResource; - - server?: WebAPI.WebAPIType; - messaging?: MessageSender; -}; - -export const textsecure: TextSecureType = { - utils, - storage: new Storage(), - - AccountManager, - EventTarget, - MessageReceiver, - MessageSender, - WebAPI, - WebSocketResource, -}; diff --git a/ts/textsecure/syncRequests.ts b/ts/textsecure/syncRequests.ts index bc6707ffcf..739e20ef9f 100644 --- a/ts/textsecure/syncRequests.ts +++ b/ts/textsecure/syncRequests.ts @@ -3,7 +3,7 @@ import { createLogger } from '../logging/log.js'; import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue.js'; -import MessageSender from './SendMessage.js'; +import { MessageSender } from './SendMessage.js'; import { toLogFormat } from '../types/errors.js'; const log = createLogger('syncRequests'); diff --git a/ts/types/LinkPreview.ts b/ts/types/LinkPreview.ts index 9c6c4e66e2..79ff083eef 100644 --- a/ts/types/LinkPreview.ts +++ b/ts/types/LinkPreview.ts @@ -56,10 +56,6 @@ export type AddLinkPreviewOptionsType = Readonly<{ const linkify = new LinkifyIt(); -export function getLinkPreviewSetting(): boolean { - return window.storage.get('linkPreviews', false); -} - export function isValidLink(maybeUrl: string | undefined): boolean { if (maybeUrl == null) { return false; diff --git a/ts/types/ServerAlert.ts b/ts/types/ServerAlert.ts new file mode 100644 index 0000000000..fc94475d70 --- /dev/null +++ b/ts/types/ServerAlert.ts @@ -0,0 +1,18 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export enum ServerAlert { + CRITICAL_IDLE_PRIMARY_DEVICE = 'critical_idle_primary_device', + IDLE_PRIMARY_DEVICE = 'idle_primary_device', +} + +export type ServerAlertsType = { + [ServerAlert.IDLE_PRIMARY_DEVICE]?: { + firstReceivedAt: number; + dismissedAt?: number; + }; + [ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE]?: { + firstReceivedAt: number; + modalLastDismissedAt?: number; + }; +}; diff --git a/ts/types/Stickers.ts b/ts/types/Stickers.ts index d300231506..76f51884a5 100644 --- a/ts/types/Stickers.ts +++ b/ts/types/Stickers.ts @@ -36,6 +36,11 @@ import { encryptLegacyAttachment } from '../util/encryptLegacyAttachment.js'; import { AttachmentDisposition } from '../util/getLocalAttachmentUrl.js'; import { isPackIdValid, redactPackId } from '../util/Stickers.js'; import { getPlaintextHashForInMemoryAttachment } from '../AttachmentCrypto.js'; +import { + isOnline, + getSticker as doGetSticker, + getStickerPackManifest, +} from '../textsecure/WebAPI.js'; const { isNumber, reject, groupBy, values, chunk } = lodash; @@ -423,12 +428,7 @@ async function downloadSticker( const { id, emoji } = proto; strictAssert(id != null, "Sticker id can't be null"); - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error('messaging is not available!'); - } - - const ciphertext = await messaging.getSticker(packId, id); + const ciphertext = await doGetSticker(packId, id); const plaintext = decryptSticker(packKey, ciphertext); const sticker = ephemeral @@ -543,12 +543,7 @@ export async function downloadEphemeralPack( }; stickerPackAdded(placeholder); - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error('messaging is not available!'); - } - - const ciphertext = await messaging.getStickerPackManifest(packId); + const ciphertext = await getStickerPackManifest(packId); const plaintext = decryptSticker(packKey, ciphertext); const proto = Proto.StickerPack.decode(plaintext); const firstStickerProto = proto.stickers ? proto.stickers[0] : null; @@ -732,13 +727,8 @@ async function doDownloadStickerPack( return; } - const { server } = window.textsecure; - if (!server) { - throw new Error('server is not available!'); - } - // We don't count this as an attempt if we're offline - const attemptIncrement = server.isOnline() ? 1 : 0; + const attemptIncrement = isOnline() ? 1 : 0; const downloadAttempts = (existing ? existing.downloadAttempts || 0 : 0) + attemptIncrement; if (downloadAttempts > 3) { @@ -780,12 +770,7 @@ async function doDownloadStickerPack( }; stickerPackAdded(placeholder); - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error('messaging is not available!'); - } - - const ciphertext = await messaging.getStickerPackManifest(packId); + const ciphertext = await getStickerPackManifest(packId); const plaintext = decryptSticker(packKey, ciphertext); const proto = Proto.StickerPack.decode(plaintext); const firstStickerProto = proto.stickers ? proto.stickers[0] : undefined; diff --git a/ts/types/Stories.ts b/ts/types/Stories.ts index cdabbb1e71..d0f9d9930f 100644 --- a/ts/types/Stories.ts +++ b/ts/types/Stories.ts @@ -179,19 +179,3 @@ export type StoryMessageRecipientsType = Array<{ distributionListIds: Array; isAllowedToReply: boolean; }>; - -export function areStoryViewReceiptsEnabled(): boolean { - return ( - window.storage.get('storyViewReceiptsEnabled') ?? - window.storage.get('read-receipt-setting') ?? - false - ); -} - -export async function setStoryViewReceiptsEnabled( - value: boolean -): Promise { - await window.storage.put('storyViewReceiptsEnabled', value); - const account = window.ConversationController.getOurConversationOrThrow(); - account.captureChange('storyViewReceiptsEnabled'); -} diff --git a/ts/types/Util.ts b/ts/types/Util.ts index a62f692153..44888810d8 100644 --- a/ts/types/Util.ts +++ b/ts/types/Util.ts @@ -115,13 +115,3 @@ export type WithRequiredProperties = Omit & export type WithOptionalProperties = Omit & Partial>; - -export function getTypingIndicatorSetting(): boolean { - return window.storage.get('typingIndicators', false); -} -export function getReadReceiptSetting(): boolean { - return window.storage.get('read-receipt-setting', false); -} -export function getSealedSenderIndicatorSetting(): boolean { - return window.storage.get('sealedSenderIndicators', false); -} diff --git a/ts/updateConversationsWithUuidLookup.ts b/ts/updateConversationsWithUuidLookup.ts index 80e5543ac4..650a04e70f 100644 --- a/ts/updateConversationsWithUuidLookup.ts +++ b/ts/updateConversationsWithUuidLookup.ts @@ -3,11 +3,16 @@ import type { ConversationController } from './ConversationController.js'; import type { ConversationModel } from './models/conversations.js'; -import type { WebAPIType } from './textsecure/WebAPI.js'; +import type { cdsLookup, checkAccountExistence } from './textsecure/WebAPI.js'; import { assertDev } from './util/assert.js'; import { isNotNil } from './util/isNotNil.js'; import { getServiceIdsForE164s } from './util/getServiceIdsForE164s.js'; +export type ServerType = Readonly<{ + cdsLookup: typeof cdsLookup; + checkAccountExistence: typeof checkAccountExistence; +}>; + export async function updateConversationsWithUuidLookup({ conversationController, conversations, @@ -18,7 +23,7 @@ export async function updateConversationsWithUuidLookup({ 'maybeMergeContacts' | 'get' >; conversations: ReadonlyArray; - server: Pick; + server: ServerType; }>): Promise { const e164s = conversations .map(conversation => conversation.get('e164')) @@ -28,7 +33,7 @@ export async function updateConversationsWithUuidLookup({ } const { entries: serverLookup, transformedE164s } = - await getServiceIdsForE164s(server, e164s); + await getServiceIdsForE164s(server.cdsLookup, e164s); await Promise.all( conversations.map(async conversation => { diff --git a/ts/util/Settings.ts b/ts/util/Settings.ts new file mode 100644 index 0000000000..248fe8e5dc --- /dev/null +++ b/ts/util/Settings.ts @@ -0,0 +1,33 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +import { itemStorage } from '../textsecure/Storage.js'; + +export function getLinkPreviewSetting(): boolean { + return itemStorage.get('linkPreviews', false); +} + +export function getTypingIndicatorSetting(): boolean { + return itemStorage.get('typingIndicators', false); +} +export function getReadReceiptSetting(): boolean { + return itemStorage.get('read-receipt-setting', false); +} +export function getSealedSenderIndicatorSetting(): boolean { + return itemStorage.get('sealedSenderIndicators', false); +} + +export function areStoryViewReceiptsEnabled(): boolean { + return ( + itemStorage.get('storyViewReceiptsEnabled') ?? + itemStorage.get('read-receipt-setting') ?? + false + ); +} + +export async function setStoryViewReceiptsEnabled( + value: boolean +): Promise { + await itemStorage.put('storyViewReceiptsEnabled', value); + const account = window.ConversationController.getOurConversationOrThrow(); + account.captureChange('storyViewReceiptsEnabled'); +} diff --git a/ts/util/areWeAdmin.ts b/ts/util/areWeAdmin.ts index aea3db20c5..cc0ce3cab9 100644 --- a/ts/util/areWeAdmin.ts +++ b/ts/util/areWeAdmin.ts @@ -4,6 +4,7 @@ import type { ConversationAttributesType } from '../model-types.js'; import { SignalService as Proto } from '../protobuf/index.js'; import { isGroupV2 } from './whatTypeOfConversation.js'; +import { itemStorage } from '../textsecure/Storage.js'; export function areWeAdmin( attributes: Pick< @@ -17,7 +18,7 @@ export function areWeAdmin( const memberEnum = Proto.Member.Role; const members = attributes.membersV2 || []; - const ourAci = window.textsecure.storage.user.getAci(); + const ourAci = itemStorage.user.getAci(); const me = members.find(item => item.aci === ourAci); if (!me) { return false; diff --git a/ts/util/attachmentDownloadQueue.ts b/ts/util/attachmentDownloadQueue.ts index 99fa5119a3..7c94ad12ec 100644 --- a/ts/util/attachmentDownloadQueue.ts +++ b/ts/util/attachmentDownloadQueue.ts @@ -13,6 +13,7 @@ import { isMoreRecentThan } from './timestamp.js'; import { isNotNil } from './isNotNil.js'; import { queueAttachmentDownloads } from './queueAttachmentDownloads.js'; import { postSaveUpdates } from './cleanup.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('attachmentDownloadQueue'); @@ -111,7 +112,7 @@ export async function flushAttachmentDownloadQueue(): Promise { .filter(isNotNil); await DataWriter.saveMessages(messagesToSave, { - ourAci: window.storage.user.getCheckedAci(), + ourAci: itemStorage.user.getCheckedAci(), postSaveUpdates, }); diff --git a/ts/util/avatarUtils.ts b/ts/util/avatarUtils.ts index a46a3e8091..0377eb31ae 100644 --- a/ts/util/avatarUtils.ts +++ b/ts/util/avatarUtils.ts @@ -6,6 +6,7 @@ import type { ContactAvatarType } from '../types/Avatar.js'; import { isMe } from './whatTypeOfConversation.js'; import { isSignalConversation } from './isSignalConversation.js'; import { getLocalAttachmentUrl } from './getLocalAttachmentUrl.js'; +import { itemStorage } from '../textsecure/Storage.js'; export function hasAvatar( conversationAttrs: ConversationAttributesType @@ -29,7 +30,7 @@ export function getAvatar( ): undefined | ContactAvatarType { const shouldShowProfileAvatar = isMe(conversationAttrs) || - window.storage.get('preferContactAvatars') === false; + itemStorage.get('preferContactAvatars') === false; const avatar = shouldShowProfileAvatar ? conversationAttrs.profileAvatar || conversationAttrs.avatar : conversationAttrs.avatar || conversationAttrs.profileAvatar; diff --git a/ts/util/backupMediaDownload.ts b/ts/util/backupMediaDownload.ts index 8b42cdadce..f2439a635f 100644 --- a/ts/util/backupMediaDownload.ts +++ b/ts/util/backupMediaDownload.ts @@ -4,18 +4,19 @@ import { AttachmentDownloadManager } from '../jobs/AttachmentDownloadManager.js'; import { createLogger } from '../logging/log.js'; import { DataWriter } from '../sql/Client.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('backupMediaDownload'); export async function startBackupMediaDownload(): Promise { - await window.storage.put('backupMediaDownloadPaused', false); + await itemStorage.put('backupMediaDownloadPaused', false); await AttachmentDownloadManager.start(); } export async function pauseBackupMediaDownload(): Promise { log.info('Pausing media download'); - await window.storage.put('backupMediaDownloadPaused', true); + await itemStorage.put('backupMediaDownloadPaused', true); } export async function resumeBackupMediaDownload(): Promise { @@ -27,10 +28,10 @@ export async function resumeBackupMediaDownload(): Promise { export async function resetBackupMediaDownloadItems(): Promise { await Promise.all([ - window.storage.remove('backupMediaDownloadTotalBytes'), - window.storage.remove('backupMediaDownloadCompletedBytes'), - window.storage.remove('backupMediaDownloadBannerDismissed'), - window.storage.remove('backupMediaDownloadPaused'), + itemStorage.remove('backupMediaDownloadTotalBytes'), + itemStorage.remove('backupMediaDownloadCompletedBytes'), + itemStorage.remove('backupMediaDownloadBannerDismissed'), + itemStorage.remove('backupMediaDownloadPaused'), ]); } @@ -47,5 +48,5 @@ export async function resetBackupMediaDownloadStats(): Promise { } export async function dismissBackupMediaDownloadBanner(): Promise { - await window.storage.put('backupMediaDownloadBannerDismissed', true); + await itemStorage.put('backupMediaDownloadBannerDismissed', true); } diff --git a/ts/util/backupSubscriptionData.ts b/ts/util/backupSubscriptionData.ts index 68c3803476..70fc332a13 100644 --- a/ts/util/backupSubscriptionData.ts +++ b/ts/util/backupSubscriptionData.ts @@ -8,6 +8,7 @@ import { backupsService } from '../services/backups/index.js'; import { drop } from './drop.js'; import { createLogger } from '../logging/log.js'; import { resetBackupMediaDownloadStats } from './backupMediaDownload.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('BackupSubscriptionData'); @@ -21,16 +22,16 @@ export async function saveBackupsSubscriberData( | null | undefined ): Promise { - const previousSubscriberId = window.storage.get('backupsSubscriberId'); + const previousSubscriberId = itemStorage.get('backupsSubscriberId'); if (previousSubscriberId !== backupsSubscriberData?.subscriberId) { drop(backupsService.refreshBackupAndSubscriptionStatus()); } if (backupsSubscriberData == null) { - await window.storage.remove('backupsSubscriberId'); - await window.storage.remove('backupsSubscriberPurchaseToken'); - await window.storage.remove('backupsSubscriberOriginalTransactionId'); + await itemStorage.remove('backupsSubscriberId'); + await itemStorage.remove('backupsSubscriberPurchaseToken'); + await itemStorage.remove('backupsSubscriberOriginalTransactionId'); return; } @@ -38,32 +39,32 @@ export async function saveBackupsSubscriberData( backupsSubscriberData; if (Bytes.isNotEmpty(subscriberId)) { - await window.storage.put('backupsSubscriberId', subscriberId); + await itemStorage.put('backupsSubscriberId', subscriberId); } else { - await window.storage.remove('backupsSubscriberId'); + await itemStorage.remove('backupsSubscriberId'); } if (purchaseToken) { - await window.storage.put('backupsSubscriberPurchaseToken', purchaseToken); + await itemStorage.put('backupsSubscriberPurchaseToken', purchaseToken); } else { - await window.storage.remove('backupsSubscriberPurchaseToken'); + await itemStorage.remove('backupsSubscriberPurchaseToken'); } if (originalTransactionId) { - await window.storage.put( + await itemStorage.put( 'backupsSubscriberOriginalTransactionId', originalTransactionId.toString() ); } else { - await window.storage.remove('backupsSubscriberOriginalTransactionId'); + await itemStorage.remove('backupsSubscriberOriginalTransactionId'); } } export async function saveBackupTier( backupTier: number | undefined ): Promise { - const previousBackupTier = window.storage.get('backupTier'); - await window.storage.put('backupTier', backupTier); + const previousBackupTier = itemStorage.get('backupTier'); + await itemStorage.put('backupTier', backupTier); if (backupTier !== previousBackupTier) { log.info('backup tier has changed', { previousBackupTier, backupTier }); await resetBackupMediaDownloadStats(); @@ -72,7 +73,7 @@ export async function saveBackupTier( } export function generateBackupsSubscriberData(): Backups.AccountData.IIAPSubscriberData | null { - const backupsSubscriberId = window.storage.get('backupsSubscriberId'); + const backupsSubscriberId = itemStorage.get('backupsSubscriberId'); if (Bytes.isEmpty(backupsSubscriberId)) { return null; @@ -81,11 +82,11 @@ export function generateBackupsSubscriberData(): Backups.AccountData.IIAPSubscri const backupsSubscriberData: Backups.AccountData.IIAPSubscriberData = { subscriberId: backupsSubscriberId, }; - const purchaseToken = window.storage.get('backupsSubscriberPurchaseToken'); + const purchaseToken = itemStorage.get('backupsSubscriberPurchaseToken'); if (purchaseToken) { backupsSubscriberData.purchaseToken = purchaseToken; } else { - const originalTransactionId = window.storage.get( + const originalTransactionId = itemStorage.get( 'backupsSubscriberOriginalTransactionId' ); if (originalTransactionId) { diff --git a/ts/util/batcher.ts b/ts/util/batcher.ts index a7eb2e3522..f14da8a550 100644 --- a/ts/util/batcher.ts +++ b/ts/util/batcher.ts @@ -12,22 +12,12 @@ import { drop } from './drop.js'; const log = createLogger('batcher'); -declare global { - // We want to extend `window`'s properties, so we need an interface. - // eslint-disable-next-line no-restricted-syntax - interface Window { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - batchers: Array>; - waitForAllBatchers: () => Promise; - } -} +let batchers = new Array>(); -window.batchers = []; - -window.waitForAllBatchers = async () => { +export const waitForAllBatchers = async (): Promise => { log.info('waitForAllBatchers'); try { - await Promise.all(window.batchers.map(item => item.flushAndWait())); + await Promise.all(batchers.map(item => item.flushAndWait())); } catch (error) { log.error( 'waitForAllBatchers: error flushing all', @@ -120,7 +110,7 @@ export function createBatcher( } function unregister() { - window.batchers = window.batchers.filter(item => item !== batcher); + batchers = batchers.filter(item => item !== batcher); } async function flushAndWait() { @@ -146,7 +136,7 @@ export function createBatcher( unregister, }; - window.batchers.push(batcher); + batchers.push(batcher as BatcherType); return batcher; } diff --git a/ts/util/callDisposition.ts b/ts/util/callDisposition.ts index 562ece78ff..47e100177e 100644 --- a/ts/util/callDisposition.ts +++ b/ts/util/callDisposition.ts @@ -49,7 +49,7 @@ import { SeenStatus, maxSeenStatus } from '../MessageSeenStatus.js'; import { canConversationBeUnarchived } from './canConversationBeUnarchived.js'; import type { ConversationAttributesType } from '../model-types.js'; import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue.js'; -import MessageSender from '../textsecure/SendMessage.js'; +import { MessageSender } from '../textsecure/SendMessage.js'; import * as Bytes from '../Bytes.js'; import type { CallDetails, @@ -71,6 +71,7 @@ import { parsePartial, parseStrict } from './schemas.js'; import { calling } from '../services/calling.js'; import { cleanupMessages } from './cleanup.js'; import { MessageModel } from '../models/messages.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { isEqual } = lodash; @@ -1300,7 +1301,7 @@ async function updateRemoteCallHistory( ); try { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const syncMessage = MessageSender.createSyncMessage(); syncMessage.callEvent = getProtoForCallHistory(callHistory); @@ -1452,7 +1453,7 @@ export async function markAllCallHistoryReadAndSync( `markAllCallHistoryReadAndSync: Marked ${count} call history messages read` ); - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const callLogEvent = new Proto.SyncMessage.CallLogEvent({ type: inConversation diff --git a/ts/util/callLinks/zkgroup.ts b/ts/util/callLinks/zkgroup.ts new file mode 100644 index 0000000000..5de4e6b54a --- /dev/null +++ b/ts/util/callLinks/zkgroup.ts @@ -0,0 +1,48 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { CallLinkRootKey } from '@signalapp/ringrtc'; +import { Aci } from '@signalapp/libsignal-client'; +import { getCheckedCallLinkAuthCredentialsForToday } from '../../services/groupCredentialFetcher.js'; +import { itemStorage } from '../../textsecure/Storage.js'; +import type { CallLinkAuthCredentialPresentation } from '../zkgroup.js'; +import * as durations from '../durations/index.js'; +import { + CallLinkAuthCredential, + CallLinkSecretParams, + GenericServerPublicParams, +} from '../zkgroup.js'; + +export async function getCallLinkAuthCredentialPresentation( + callLinkRootKey: CallLinkRootKey +): Promise { + const credentials = getCheckedCallLinkAuthCredentialsForToday( + 'getCallLinkAuthCredentialPresentation' + ); + const todaysCredentials = credentials.today.credential; + const credential = new CallLinkAuthCredential( + Buffer.from(todaysCredentials, 'base64') + ); + + const genericServerPublicParamsBase64 = window.getGenericServerPublicParams(); + const genericServerPublicParams = new GenericServerPublicParams( + Buffer.from(genericServerPublicParamsBase64, 'base64') + ); + + const ourAci = itemStorage.user.getAci(); + if (ourAci == null) { + throw new Error('Failed to get our ACI'); + } + const userId = Aci.fromUuid(ourAci); + + const callLinkSecretParams = CallLinkSecretParams.deriveFromRootKey( + callLinkRootKey.bytes + ); + const presentation = credential.present( + userId, + credentials.today.redemptionTime / durations.SECOND, + genericServerPublicParams, + callLinkSecretParams + ); + return presentation; +} diff --git a/ts/util/callLinksRingrtc.ts b/ts/util/callLinksRingrtc.ts index 84b296d599..0cb8262b92 100644 --- a/ts/util/callLinksRingrtc.ts +++ b/ts/util/callLinksRingrtc.ts @@ -8,7 +8,6 @@ import { } from '@signalapp/ringrtc'; import type { CallLinkState as RingRTCCallLinkState } from '@signalapp/ringrtc'; import { z } from 'zod'; -import { Aci } from '@signalapp/libsignal-client'; import { CallLinkNameMaxByteLength, callLinkRecordSchema, @@ -24,14 +23,6 @@ import type { CallLinkStateType, } from '../types/CallLink.js'; import { unicodeSlice } from './unicodeSlice.js'; -import type { CallLinkAuthCredentialPresentation } from './zkgroup.js'; -import { - CallLinkAuthCredential, - CallLinkSecretParams, - GenericServerPublicParams, -} from './zkgroup.js'; -import { getCheckedCallLinkAuthCredentialsForToday } from '../services/groupCredentialFetcher.js'; -import * as durations from './durations/index.js'; import { fromAdminKeyBytes, getKeyFromCallLink, @@ -85,40 +76,6 @@ export function getRoomIdFromCallLink(url: string): string { return getRoomIdFromRootKey(key); } -export async function getCallLinkAuthCredentialPresentation( - callLinkRootKey: CallLinkRootKey -): Promise { - const credentials = getCheckedCallLinkAuthCredentialsForToday( - 'getCallLinkAuthCredentialPresentation' - ); - const todaysCredentials = credentials.today.credential; - const credential = new CallLinkAuthCredential( - Buffer.from(todaysCredentials, 'base64') - ); - - const genericServerPublicParamsBase64 = window.getGenericServerPublicParams(); - const genericServerPublicParams = new GenericServerPublicParams( - Buffer.from(genericServerPublicParamsBase64, 'base64') - ); - - const ourAci = window.textsecure.storage.user.getAci(); - if (ourAci == null) { - throw new Error('Failed to get our ACI'); - } - const userId = Aci.fromUuid(ourAci); - - const callLinkSecretParams = CallLinkSecretParams.deriveFromRootKey( - callLinkRootKey.bytes - ); - const presentation = credential.present( - userId, - credentials.today.redemptionTime / durations.SECOND, - genericServerPublicParams, - callLinkSecretParams - ); - return presentation; -} - export function toRootKeyBytes(rootKey: string): Uint8Array { return CallLinkRootKey.parse(rootKey).bytes; } diff --git a/ts/util/callingTones.ts b/ts/util/callingTones.ts index c4cf3bd625..7c6a9c8653 100644 --- a/ts/util/callingTones.ts +++ b/ts/util/callingTones.ts @@ -4,6 +4,7 @@ import PQueue from 'p-queue'; import { MINUTE } from './durations/index.js'; import { Sound, SoundType } from './Sound.js'; +import { itemStorage } from '../textsecure/Storage.js'; const ringtoneEventQueue = new PQueue({ concurrency: 1, @@ -12,7 +13,7 @@ const ringtoneEventQueue = new PQueue({ }); function getCallRingtoneNotificationSetting(): boolean { - return window.storage.get('call-ringtone-notification', true); + return itemStorage.get('call-ringtone-notification', true); } class CallingTones { diff --git a/ts/util/canConversationBeUnarchived.ts b/ts/util/canConversationBeUnarchived.ts index 348d8693ae..608acc0200 100644 --- a/ts/util/canConversationBeUnarchived.ts +++ b/ts/util/canConversationBeUnarchived.ts @@ -3,6 +3,7 @@ import type { ConversationAttributesType } from '../model-types.d.ts'; import { isConversationMuted } from './isConversationMuted.js'; +import { itemStorage } from '../textsecure/Storage.js'; export function canConversationBeUnarchived( attrs: ConversationAttributesType @@ -15,7 +16,7 @@ export function canConversationBeUnarchived( return true; } - if (window.storage.get('keepMutedChatsArchived') ?? false) { + if (itemStorage.get('keepMutedChatsArchived') ?? false) { return false; } diff --git a/ts/util/checkOurPniIdentityKey.ts b/ts/util/checkOurPniIdentityKey.ts index 2c351a2511..d2063949a6 100644 --- a/ts/util/checkOurPniIdentityKey.ts +++ b/ts/util/checkOurPniIdentityKey.ts @@ -4,16 +4,14 @@ import { createLogger } from '../logging/log.js'; import { constantTimeEqual } from '../Crypto.js'; import { signalProtocolStore } from '../SignalProtocolStore.js'; -import { strictAssert } from './assert.js'; +import { whoami, getKeysForServiceId } from '../textsecure/WebAPI.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('checkOurPniIdentityKey'); export async function checkOurPniIdentityKey(): Promise { - const { server } = window.textsecure; - strictAssert(server, 'WebAPI not ready'); - - const ourPni = window.storage.user.getCheckedPni(); - const { pni: remotePni } = await server.whoami(); + const ourPni = itemStorage.user.getCheckedPni(); + const { pni: remotePni } = await whoami(); if (remotePni !== ourPni) { log.warn(`remote pni mismatch, ${remotePni} != ${ourPni}`); window.Whisper.events.emit('unlinkAndDisconnect'); @@ -27,7 +25,7 @@ export async function checkOurPniIdentityKey(): Promise { return; } - const { identityKey: remoteKey } = await server.getKeysForServiceId(ourPni); + const { identityKey: remoteKey } = await getKeysForServiceId(ourPni); if (!constantTimeEqual(localKeyPair.publicKey.serialize(), remoteKey)) { log.warn(`local/remote key mismatch for ${ourPni}, unlinking`); window.Whisper.events.emit('unlinkAndDisconnect'); diff --git a/ts/util/cleanup.ts b/ts/util/cleanup.ts index 0c002909a7..1dd4883c4e 100644 --- a/ts/util/cleanup.ts +++ b/ts/util/cleanup.ts @@ -10,6 +10,7 @@ import { MessageModel } from '../models/messages.js'; import * as Errors from '../types/errors.js'; import { createLogger } from '../logging/log.js'; +import { MessageSender } from '../textsecure/SendMessage.js'; import { DataReader, DataWriter } from '../sql/Client.js'; import { deletePackReference } from '../types/Stickers.js'; import { isStory } from '../messages/helpers.js'; @@ -260,7 +261,7 @@ export async function maybeDeleteCall( if (!fromSync) { await singleProtoJobQueue.add( - window.textsecure.MessageSender.getDeleteCallEvent(callHistory) + MessageSender.getDeleteCallEvent(callHistory) ); } await DataWriter.markCallHistoryDeleted(callId); diff --git a/ts/util/createIPCEvents.ts b/ts/util/createIPCEvents.ts index 55863073ec..a31461fd41 100644 --- a/ts/util/createIPCEvents.ts +++ b/ts/util/createIPCEvents.ts @@ -13,7 +13,6 @@ import * as Settings from '../types/Settings.js'; import { resolveUsernameByLinkBase64 } from '../services/username.js'; import { isInCall } from '../state/selectors/calling.js'; -import { strictAssert } from './assert.js'; import * as Registration from './registration.js'; import { lookupConversationWithoutServiceId } from './lookupConversationWithoutServiceId.js'; import { createLogger } from '../logging/log.js'; @@ -32,6 +31,7 @@ import type { ThemeType, } from './preload.js'; import { SystemTraySetting } from '../types/SystemTraySetting.js'; +import { putStickers } from '../textsecure/WebAPI.js'; import OS from './os/osPreload.js'; const { noop } = lodash; @@ -438,8 +438,7 @@ export function createIPCEvents( manifest: Uint8Array, stickers: ReadonlyArray ): Promise => { - strictAssert(window.textsecure.server, 'WebAPI must be available'); - return window.textsecure.server.putStickers(manifest, stickers, () => + return putStickers(manifest, stickers, () => ipcRenderer.send('art-creator:onUploadProgress') ); }, diff --git a/ts/util/deleteForEveryone.ts b/ts/util/deleteForEveryone.ts index d47feb676b..a81cca7c44 100644 --- a/ts/util/deleteForEveryone.ts +++ b/ts/util/deleteForEveryone.ts @@ -5,7 +5,7 @@ import type { DeleteAttributesType } from '../messageModifiers/Deletes.js'; import type { MessageModel } from '../models/messages.js'; import { createLogger } from '../logging/log.js'; import { isMe } from './whatTypeOfConversation.js'; -import { getAuthorId } from '../messages/helpers.js'; +import { getAuthorId } from '../messages/sources.js'; import { isStory } from '../state/selectors/message.js'; import { isTooOldToModifyMessage } from './isTooOldToModifyMessage.js'; import { drop } from './drop.js'; diff --git a/ts/util/deleteForMe.ts b/ts/util/deleteForMe.ts index aa38152531..e35fd35a4c 100644 --- a/ts/util/deleteForMe.ts +++ b/ts/util/deleteForMe.ts @@ -17,6 +17,7 @@ import { findMatchingMessage, getMessageQueryFromTarget, } from './syncIdentifiers.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { last, sortBy } = lodash; @@ -122,7 +123,7 @@ export async function applyDeleteAttachmentFromMessage( return true; } - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const attachments = message.get('attachments'); if (!attachments || attachments.length === 0) { diff --git a/ts/util/deleteStoryForEveryone.ts b/ts/util/deleteStoryForEveryone.ts index 5f83a38559..4cd57a0580 100644 --- a/ts/util/deleteStoryForEveryone.ts +++ b/ts/util/deleteStoryForEveryone.ts @@ -20,7 +20,6 @@ import { onStoryRecipientUpdate } from './onStoryRecipientUpdate.js'; import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage.js'; import { isGroupV2 } from './whatTypeOfConversation.js'; import { getMessageById } from '../messages/getMessageById.js'; -import { strictAssert } from './assert.js'; import { repeat, zipObject } from './iterables.js'; import { isOlderThan } from './timestamp.js'; @@ -156,10 +155,6 @@ export async function deleteStoryForEveryone( }); }); - // Include the sync message with the updated storyMessageRecipients list - const sender = window.textsecure.messaging; - strictAssert(sender, 'messaging has to be initialized'); - const newStoryMessageRecipients: StoryMessageRecipientsType = []; newStoryRecipients.forEach((recipientData, destinationServiceId) => { diff --git a/ts/util/denyPendingApprovalRequest.ts b/ts/util/denyPendingApprovalRequest.ts index 28d8c452b4..1426a645a6 100644 --- a/ts/util/denyPendingApprovalRequest.ts +++ b/ts/util/denyPendingApprovalRequest.ts @@ -8,6 +8,7 @@ import { createLogger } from '../logging/log.js'; import { buildDeletePendingAdminApprovalMemberChange } from '../groups.js'; import { getConversationIdForLogging } from './idForLogging.js'; import { isMemberRequestingToJoin } from './groupMembershipUtils.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('denyPendingApprovalRequest'); @@ -28,7 +29,7 @@ export async function denyPendingApprovalRequest( return undefined; } - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); return buildDeletePendingAdminApprovalMemberChange({ group: conversationAttributes, diff --git a/ts/util/doubleCheckMissingQuoteReference.ts b/ts/util/doubleCheckMissingQuoteReference.ts index 4623fc1576..d4fe3306b2 100644 --- a/ts/util/doubleCheckMissingQuoteReference.ts +++ b/ts/util/doubleCheckMissingQuoteReference.ts @@ -8,10 +8,8 @@ import { hydrateStoryContext } from './hydrateStoryContext.js'; import { getMessageIdForLogging } from './idForLogging.js'; import { createLogger } from '../logging/log.js'; -import { - isQuoteAMatch, - shouldTryToCopyFromQuotedMessage, -} from '../messages/helpers.js'; +import { isQuoteAMatch } from '../messages/quotes.js'; +import { shouldTryToCopyFromQuotedMessage } from '../messages/helpers.js'; import { copyQuoteContentFromOriginal } from '../messages/copyQuote.js'; import { queueUpdateMessage } from './messageBatcher.js'; import { drop } from './drop.js'; diff --git a/ts/util/downloadAttachment.ts b/ts/util/downloadAttachment.ts index e4bec1d8ce..11d167b3b9 100644 --- a/ts/util/downloadAttachment.ts +++ b/ts/util/downloadAttachment.ts @@ -11,6 +11,10 @@ import { AttachmentPermanentlyUndownloadableError, } from '../types/Attachment.js'; import { downloadAttachment as doDownloadAttachment } from '../textsecure/downloadAttachment.js'; +import { + getAttachment, + getAttachmentFromBackupTier, +} from '../textsecure/WebAPI.js'; import { downloadAttachmentFromLocalBackup as doDownloadAttachmentFromLocalBackup } from './downloadAttachmentFromLocalBackup.js'; import { MediaTier } from '../types/AttachmentDownload.js'; import { createLogger } from '../logging/log.js'; @@ -54,11 +58,6 @@ export async function downloadAttachment({ variant !== AttachmentVariant.Default ? `[${variant}]` : ''; const logId = `${_logId}${variantForLogging}`; - const { server } = window.textsecure; - if (!server) { - throw new Error('window.textsecure.server is not available!'); - } - const isBackupable = hasRequiredInformationForBackup(attachment); const mightBeOnBackupTierNow = isBackupable && hasMediaBackups; @@ -89,7 +88,10 @@ export async function downloadAttachment({ if (mightBeOnBackupTierNow) { try { return await dependencies.downloadAttachmentFromServer( - server, + { + getAttachment, + getAttachmentFromBackupTier, + }, { mediaTier: MediaTier.BACKUP, attachment }, { logId, @@ -140,7 +142,10 @@ export async function downloadAttachment({ try { return await dependencies.downloadAttachmentFromServer( - server, + { + getAttachment, + getAttachmentFromBackupTier, + }, { attachment, mediaTier: MediaTier.STANDARD }, { logId, diff --git a/ts/util/downloadOnboardingStory.ts b/ts/util/downloadOnboardingStory.ts index 66613f73d3..2d520e04fd 100644 --- a/ts/util/downloadOnboardingStory.ts +++ b/ts/util/downloadOnboardingStory.ts @@ -11,8 +11,12 @@ import { ReadStatus } from '../messages/MessageReadStatus.js'; import { SeenStatus } from '../MessageSeenStatus.js'; import { findAndDeleteOnboardingStoryIfExists } from './findAndDeleteOnboardingStoryIfExists.js'; import { saveNewMessageBatcher } from './messageBatcher.js'; -import { strictAssert } from './assert.js'; import { incrementMessageCounter } from './incrementMessageCounter.js'; +import { + getOnboardingStoryManifest, + downloadOnboardingStories, +} from '../textsecure/WebAPI.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('downloadOnboardingStory'); @@ -25,20 +29,14 @@ const log = createLogger('downloadOnboardingStory'); // * If story has been viewed mark as viewed on AccountRecord. // * If we viewed it >24 hours ago, delete. export async function downloadOnboardingStory(): Promise { - const { server } = window.textsecure; - - strictAssert(server, 'server not initialized'); - - const hasViewedOnboardingStory = window.storage.get( - 'hasViewedOnboardingStory' - ); + const hasViewedOnboardingStory = itemStorage.get('hasViewedOnboardingStory'); if (hasViewedOnboardingStory) { await findAndDeleteOnboardingStoryIfExists(); return; } - const existingOnboardingStoryMessageIds = window.storage.get( + const existingOnboardingStoryMessageIds = itemStorage.get( 'existingOnboardingStoryMessageIds' ); @@ -49,7 +47,7 @@ export async function downloadOnboardingStory(): Promise { const userLocale = window.i18n.getLocale(); - const manifest = await server.getOnboardingStoryManifest(); + const manifest = await getOnboardingStoryManifest(); log.info('got manifest version:', manifest.version); @@ -58,7 +56,7 @@ export async function downloadOnboardingStory(): Promise { ? manifest.languages[userLocale] : manifest.languages.en; - const imageBuffers = await server.downloadOnboardingStories( + const imageBuffers = await downloadOnboardingStories( manifest.version, imageFilenames ); @@ -112,7 +110,7 @@ export async function downloadOnboardingStory(): Promise { storyMessages.map(message => saveNewMessageBatcher.add(message.attributes)) ); - await window.storage.put( + await itemStorage.put( 'existingOnboardingStoryMessageIds', storyMessages.map(message => message.id) ); diff --git a/ts/util/encryptConversationAttachments.ts b/ts/util/encryptConversationAttachments.ts index 0120bbf470..366420dfe5 100644 --- a/ts/util/encryptConversationAttachments.ts +++ b/ts/util/encryptConversationAttachments.ts @@ -11,6 +11,7 @@ import { AttachmentDisposition } from './getLocalAttachmentUrl.js'; import { isNotNil } from './isNotNil.js'; import { isSignalConversation } from './isSignalConversation.js'; import { getConversationIdForLogging } from './idForLogging.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('encryptConversationAttachments'); @@ -174,7 +175,7 @@ async function encryptOne(attributes: ConversationAttributesType): Promise< // Just drop thumbnail reference. It is impossible to recover, and has // minimal UI impact. if (!path.startsWith('attachment://')) { - await window.storage.put('needOrphanedAttachmentCheck', true); + await itemStorage.put('needOrphanedAttachmentCheck', true); if (result.draftEditMessage) { result.draftEditMessage.attachmentThumbnail = undefined; } diff --git a/ts/util/encryptLegacyAttachment.ts b/ts/util/encryptLegacyAttachment.ts index f8981cb8b0..1daef04531 100644 --- a/ts/util/encryptLegacyAttachment.ts +++ b/ts/util/encryptLegacyAttachment.ts @@ -12,6 +12,7 @@ import { DataWriter } from '../sql/Client.js'; import { AttachmentDisposition } from './getLocalAttachmentUrl.js'; import { drop } from './drop.js'; import { MINUTE } from './durations/index.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('encryptLegacyAttachment'); @@ -92,7 +93,7 @@ async function doEncrypt>( cleanup(); } else if (!setCheck) { setCheck = true; - await window.storage.put('needOrphanedAttachmentCheck', true); + await itemStorage.put('needOrphanedAttachmentCheck', true); log.error('scheduling orphaned cleanup'); cleanupTimeout = setTimeout(cleanup, 15 * MINUTE); } @@ -106,6 +107,6 @@ function cleanup(): void { cleanupTimeout = undefined; setCheck = false; orphanedCount = 0; - drop(window.storage.remove('needOrphanedAttachmentCheck')); + drop(itemStorage.remove('needOrphanedAttachmentCheck')); drop(DataWriter.cleanupOrphanedAttachments()); } diff --git a/ts/util/encryptProfileData.ts b/ts/util/encryptProfileData.ts index 8e1fcd2f11..a6260ee7e3 100644 --- a/ts/util/encryptProfileData.ts +++ b/ts/util/encryptProfileData.ts @@ -16,6 +16,7 @@ import { deriveProfileKeyVersion, } from './zkgroup.js'; import { isSharingPhoneNumberWithEverybody } from './phoneNumberSharingMode.js'; +import { itemStorage } from '../textsecure/Storage.js'; export async function encryptProfileData( conversation: ConversationType, @@ -79,7 +80,7 @@ export async function encryptProfileData( badgeIds: (badges || []) .filter(badge => 'isVisible' in badge && badge.isVisible) .map(({ id }) => id), - paymentAddress: window.storage.get('paymentAddress') || null, + paymentAddress: itemStorage.get('paymentAddress') || null, avatar: Boolean(newAvatar), sameAvatar, commitment: deriveProfileKeyCommitment(profileKey, serviceId), diff --git a/ts/util/filterAndSortConversations.ts b/ts/util/filterAndSortConversations.ts index 808785db9b..9b427d4b49 100644 --- a/ts/util/filterAndSortConversations.ts +++ b/ts/util/filterAndSortConversations.ts @@ -100,10 +100,7 @@ COMMANDS.set('groupIdEndsWith', (conversations, query) => { }); COMMANDS.set('unread', (conversations, query) => { - const includeMuted = - /^(?:m|muted)$/i.test(query) || - window.storage.get('badge-count-muted-conversations') || - false; + const includeMuted = /^(?:m|muted)$/i.test(query) || false; return filterConversationsByUnread(conversations, includeMuted); }); diff --git a/ts/util/findAndDeleteOnboardingStoryIfExists.ts b/ts/util/findAndDeleteOnboardingStoryIfExists.ts index 67bf58237d..d390ba7ee4 100644 --- a/ts/util/findAndDeleteOnboardingStoryIfExists.ts +++ b/ts/util/findAndDeleteOnboardingStoryIfExists.ts @@ -7,11 +7,12 @@ import { calculateExpirationTimestamp } from './expirationTimer.js'; import { DAY } from './durations/index.js'; import { cleanupMessages } from './cleanup.js'; import { getMessageById } from '../messages/getMessageById.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('findAndDeleteOnboardingStoryIfExists'); export async function findAndDeleteOnboardingStoryIfExists(): Promise { - const existingOnboardingStoryMessageIds = window.storage.get( + const existingOnboardingStoryMessageIds = itemStorage.get( 'existingOnboardingStoryMessageIds' ); @@ -52,7 +53,7 @@ export async function findAndDeleteOnboardingStoryIfExists(): Promise { cleanupMessages, }); - await window.storage.put('existingOnboardingStoryMessageIds', undefined); + await itemStorage.put('existingOnboardingStoryMessageIds', undefined); const signalConversation = await window.ConversationController.getOrCreateSignalConversation(); diff --git a/ts/util/findAndFormatContact.ts b/ts/util/findAndFormatContact.ts index 2732b40c8f..4d46b7d571 100644 --- a/ts/util/findAndFormatContact.ts +++ b/ts/util/findAndFormatContact.ts @@ -4,6 +4,7 @@ import type { ConversationType } from '../state/ducks/conversations.js'; import { PLACEHOLDER_CONTACT_ID } from '../state/selectors/conversations.js'; import { format, isValidNumber } from '../types/PhoneNumber.js'; +import { itemStorage } from '../textsecure/Storage.js'; const PLACEHOLDER_CONTACT: ConversationType = { acceptedMessageRequest: false, @@ -27,7 +28,7 @@ export function findAndFormatContact(identifier?: string): ConversationType { return contactModel.format(); } - const regionCode = window.storage.get('regionCode'); + const regionCode = itemStorage.get('regionCode'); if (!isValidNumber(identifier, { regionCode })) { return PLACEHOLDER_CONTACT; diff --git a/ts/util/findStoryMessage.ts b/ts/util/findStoryMessage.ts index 174ac63f70..4eb4baa65c 100644 --- a/ts/util/findStoryMessage.ts +++ b/ts/util/findStoryMessage.ts @@ -9,7 +9,7 @@ import { type AciString } from '../types/ServiceId.js'; import type { ProcessedStoryContext } from '../textsecure/Types.d.ts'; import { DataReader } from '../sql/Client.js'; import { createLogger } from '../logging/log.js'; -import { getAuthorId } from '../messages/helpers.js'; +import { getAuthorId } from '../messages/sources.js'; const log = createLogger('findStoryMessage'); diff --git a/ts/util/getAddedByForOurPendingInvitation.ts b/ts/util/getAddedByForOurPendingInvitation.ts index 093daff9d8..58a0cf8af8 100644 --- a/ts/util/getAddedByForOurPendingInvitation.ts +++ b/ts/util/getAddedByForOurPendingInvitation.ts @@ -1,12 +1,13 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ConversationType } from '../state/ducks/conversations.js'; +import { itemStorage } from '../textsecure/Storage.js'; export function getAddedByForOurPendingInvitation( conversation: ConversationType ): ConversationType | null { - const ourAci = window.storage.user.getCheckedAci(); - const ourPni = window.storage.user.getPni(); + const ourAci = itemStorage.user.getCheckedAci(); + const ourPni = itemStorage.user.getPni(); const addedBy = conversation.pendingMemberships?.find( item => item.serviceId === ourAci || item.serviceId === ourPni )?.addedByUserId; diff --git a/ts/util/getConversation.ts b/ts/util/getConversation.ts index 9fc3f37365..c6197e05e8 100644 --- a/ts/util/getConversation.ts +++ b/ts/util/getConversation.ts @@ -58,6 +58,7 @@ import { import { isNotNil } from './isNotNil.js'; import { getIdentifierHash } from '../Crypto.js'; import { getAvatarPlaceholderGradient } from '../utils/getAvatarPlaceholderGradient.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { sortBy } = lodash; @@ -91,7 +92,7 @@ export function getConversation(model: ConversationModel): ConversationType { typingValues.map(({ senderId, timestamp }) => [senderId, timestamp]) ); - const ourAci = window.textsecure.storage.user.getAci(); + const ourAci = itemStorage.user.getAci(); const identifierHash = getIdentifierHash({ aci: isAciString(attributes.serviceId) ? attributes.serviceId : undefined, diff --git a/ts/util/getNotificationDataForMessage.ts b/ts/util/getNotificationDataForMessage.ts index c4ddb54e18..c3b6dd2021 100644 --- a/ts/util/getNotificationDataForMessage.ts +++ b/ts/util/getNotificationDataForMessage.ts @@ -49,14 +49,15 @@ import { isConversationMerge, isMessageRequestResponse, } from '../state/selectors/message.js'; +import { getAuthor } from '../messages/sources.js'; import { - getAuthor, messageHasPaymentEvent, getPaymentEventNotificationText, -} from '../messages/helpers.js'; +} from '../messages/payments.js'; import { MessageRequestResponseEvent } from '../types/MessageRequestResponseEvent.js'; import { missingCaseError } from './missingCaseError.js'; import { getUserConversationId } from '../state/selectors/user.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('getNotificationDataForMessage'); @@ -149,8 +150,8 @@ export function getNotificationDataForMessage( const changes = GroupChange.renderChange(change, { i18n: window.i18n, - ourAci: window.textsecure.storage.user.getCheckedAci(), - ourPni: window.textsecure.storage.user.getCheckedPni(), + ourAci: itemStorage.user.getCheckedAci(), + ourPni: itemStorage.user.getCheckedPni(), renderContact: (conversationId: string) => { const conversation = window.ConversationController.get(conversationId); return conversation diff --git a/ts/util/getNotificationTextForMessage.ts b/ts/util/getNotificationTextForMessage.ts index b25e8f1c66..179a4e4a57 100644 --- a/ts/util/getNotificationTextForMessage.ts +++ b/ts/util/getNotificationTextForMessage.ts @@ -7,6 +7,7 @@ import { findAndFormatContact } from './findAndFormatContact.js'; import { getNotificationDataForMessage } from './getNotificationDataForMessage.js'; import { isConversationAccepted } from './isConversationAccepted.js'; import { strictAssert } from './assert.js'; +import { itemStorage } from '../textsecure/Storage.js'; export function getNotificationTextForMessage( attributes: ReadonlyMessageAttributesType @@ -45,7 +46,7 @@ export function getNotificationTextForMessage( }); } - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); if ( attributes.type === 'incoming' && diff --git a/ts/util/getServiceIdsForE164s.ts b/ts/util/getServiceIdsForE164s.ts index 64d25ba219..a86361d094 100644 --- a/ts/util/getServiceIdsForE164s.ts +++ b/ts/util/getServiceIdsForE164s.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { CDSResponseType } from '../textsecure/cds/Types.d.ts'; -import type { WebAPIType } from '../textsecure/WebAPI.js'; +import type { cdsLookup } from '../textsecure/WebAPI.js'; import type { AciString } from '../types/ServiceId.js'; import { createLogger } from '../logging/log.js'; import { isDirectConversation, isMe } from './whatTypeOfConversation.js'; @@ -50,7 +50,7 @@ type ReturnType = CDSResponseType & { }; export async function getServiceIdsForE164s( - server: Pick, + doCdsLookup: typeof cdsLookup, e164s: ReadonlyArray ): Promise { const expandedE164s = new Set(e164s); @@ -105,7 +105,7 @@ export async function getServiceIdsForE164s( `getServiceIdsForE164s(${expandedE164sArray}): acis=${acisAndAccessKeys.length} ` + `accessKeys=${acisAndAccessKeys.length}` ); - const response = await server.cdsLookup({ + const response = await doCdsLookup({ e164s: expandedE164sArray, acisAndAccessKeys, returnAcisWithoutUaks: false, diff --git a/ts/util/getTitle.ts b/ts/util/getTitle.ts index 56cd40011b..d786be6b52 100644 --- a/ts/util/getTitle.ts +++ b/ts/util/getTitle.ts @@ -10,6 +10,7 @@ import { getRegionCodeForNumber } from './libphonenumberUtil.js'; import { instance, PhoneNumberFormat } from './libphonenumberInstance.js'; import { isDirectConversation } from './whatTypeOfConversation.js'; import { getE164 } from './getE164.js'; +import { itemStorage } from '../textsecure/Storage.js'; type TitleOptions = { isShort?: boolean; @@ -161,7 +162,7 @@ export function renderNumber(e164: string): string | undefined { try { const parsedNumber = instance.parse(e164); const regionCode = getRegionCodeForNumber(e164); - if (regionCode === window.storage.get('regionCode')) { + if (regionCode === itemStorage.get('regionCode')) { return instance.format(parsedNumber, PhoneNumberFormat.NATIONAL); } return instance.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL); diff --git a/ts/util/groupMembershipUtils.ts b/ts/util/groupMembershipUtils.ts index 14aa382300..360630bdf0 100644 --- a/ts/util/groupMembershipUtils.ts +++ b/ts/util/groupMembershipUtils.ts @@ -6,6 +6,7 @@ import type { ConversationAttributesType } from '../model-types.d.ts'; import type { ServiceIdString, AciString } from '../types/ServiceId.js'; import { SignalService as Proto } from '../protobuf/index.js'; import { isDirectConversation, isGroupV2 } from './whatTypeOfConversation.js'; +import { itemStorage } from '../textsecure/Storage.js'; export function isMemberPending( conversationAttrs: Pick< @@ -191,8 +192,8 @@ export function areWePending( 'groupId' | 'groupVersion' | 'pendingMembersV2' > ): boolean { - const ourAci = window.textsecure.storage.user.getAci(); - const ourPni = window.textsecure.storage.user.getPni(); + const ourAci = itemStorage.user.getAci(); + const ourPni = itemStorage.user.getPni(); return Boolean( ourAci && (isMemberPending(conversationAttrs, ourAci) || diff --git a/ts/util/groupSendEndorsements.ts b/ts/util/groupSendEndorsements.ts index ca9f83ec26..89785e90b8 100644 --- a/ts/util/groupSendEndorsements.ts +++ b/ts/util/groupSendEndorsements.ts @@ -33,6 +33,7 @@ import { DataReader } from '../sql/Client.js'; import { maybeUpdateGroup } from '../groups.js'; import * as Bytes from '../Bytes.js'; import { isGroupV2 } from './whatTypeOfConversation.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { throttle } = lodash; @@ -68,7 +69,7 @@ export function decodeGroupSendEndorsementResponse({ const expiration = response.getExpiration().getTime() / 1000; const localUser = Aci.parseFromServiceIdString( - window.textsecure.storage.user.getCheckedAci() + itemStorage.user.getCheckedAci() ); const groupSecretParams = new GroupSecretParams( @@ -178,7 +179,7 @@ export class GroupSendEndorsementState { this.#logId = `GroupSendEndorsementState/groupv2(${data.combinedEndorsement.groupId})`; this.#combinedEndorsement = data.combinedEndorsement; this.#groupSecretParamsBase64 = groupSecretParamsBase64; - this.#ourAci = window.textsecure.storage.user.getCheckedAci(); + this.#ourAci = itemStorage.user.getCheckedAci(); for (const endorsement of data.memberEndorsements) { this.#memberEndorsements.set(endorsement.memberAci, endorsement); this.#memberEndorsementsAcis.add(endorsement.memberAci); @@ -356,7 +357,7 @@ function validateGroupSendEndorsements( ): ValidationResult { // Check if we should have endorsements (pending members should not) if (state == null) { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); if (members.has(ourAci)) { return { valid: false, reason: 'missing all endorsements' }; } diff --git a/ts/util/handleEditMessage.ts b/ts/util/handleEditMessage.ts index 0222ffe98d..2a0bbd7593 100644 --- a/ts/util/handleEditMessage.ts +++ b/ts/util/handleEditMessage.ts @@ -28,6 +28,7 @@ import { queueAttachmentDownloads } from './queueAttachmentDownloads.js'; import { modifyTargetMessage } from './modifyTargetMessage.js'; import { isMessageNoteToSelf } from './isMessageNoteToSelf.js'; import { MessageModel } from '../models/messages.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('handleEditMessage'); @@ -54,7 +55,7 @@ export async function handleEditMessage( // Use local aci for outgoing messages and sourceServiceId for incoming. const senderAci = isOutgoing(mainMessage) - ? window.storage.user.getCheckedAci() + ? itemStorage.user.getCheckedAci() : mainMessage.sourceServiceId; if (!isAciString(senderAci)) { log.warn(`${idLog}: Cannot edit a message from PNI source`); @@ -336,7 +337,7 @@ export async function handleEditMessage( drop( DataWriter.saveEditedMessage( mainMessageModel.attributes, - window.textsecure.storage.user.getCheckedAci(), + itemStorage.user.getCheckedAci(), { conversationId: editAttributes.conversationId, messageId: mainMessage.id, diff --git a/ts/util/handleRetry.ts b/ts/util/handleRetry.ts index 5e26cb8e3b..784934fd82 100644 --- a/ts/util/handleRetry.ts +++ b/ts/util/handleRetry.ts @@ -36,13 +36,17 @@ import type { import { SignalService as Proto } from '../protobuf/index.js'; import { createLogger } from '../logging/log.js'; -import type MessageSender from '../textsecure/SendMessage.js'; +import { + type MessageSender, + messageSender, +} from '../textsecure/SendMessage.js'; import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists.js'; import { drop } from './drop.js'; import { conversationJobQueue } from '../jobs/conversationJobQueue.js'; import { incrementMessageCounter } from './incrementMessageCounter.js'; import { SECOND } from './durations/index.js'; import { sleep } from './sleep.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { isNumber, random } = lodash; @@ -208,11 +212,6 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise { log.info(`onRetryRequest/${logId}: Resending message`); - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error(`onRetryRequest/${logId}: messaging is not available!`); - } - const { contentHint, messageIds, proto, timestamp, urgent } = sentProto; // Only applies to sender key sends in groups. See below for story distribution lists. @@ -236,7 +235,7 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise { confirm, contentProto, logId, - messaging, + messaging: messageSender, requesterAci, timestamp, }); @@ -349,14 +348,14 @@ async function archiveSessionOnMatch({ senderDevice, }: RetryRequestEventData): Promise { const ourDeviceId = parseIntOrThrow( - window.textsecure.storage.user.getDeviceId(), + itemStorage.user.getDeviceId(), 'archiveSessionOnMatch/getDeviceId' ); if (ourDeviceId !== senderDevice || !ratchetKey) { return false; } - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const address = new QualifiedAddress( ourAci, Address.create(requesterAci, requesterDevice) @@ -383,13 +382,6 @@ async function sendDistributionMessageOrNullMessage( let sentDistributionMessage = false; log.info(`sendDistributionMessageOrNullMessage/${logId}: Starting...`); - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error( - `sendDistributionMessageOrNullMessage/${logId}: messaging is not available!` - ); - } - const conversation = window.ConversationController.getOrCreate( requesterAci, 'private' @@ -586,13 +578,6 @@ async function maybeAddSenderKeyDistributionMessage({ requestGroupId, }); - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error( - `maybeAddSenderKeyDistributionMessage/${logId}: messaging is not available!` - ); - } - if (!conversation) { log.warn( `maybeAddSenderKeyDistributionMessage/${logId}: Unable to find conversation` @@ -617,7 +602,7 @@ async function maybeAddSenderKeyDistributionMessage({ const senderKeyInfo = conversation.get('senderKeyInfo'); if (senderKeyInfo && senderKeyInfo.distributionId) { const protoWithDistributionMessage = - await messaging.getSenderKeyDistributionMessage( + await messageSender.getSenderKeyDistributionMessage( senderKeyInfo.distributionId, { throwIfNotInDatabase: true, timestamp } ); @@ -659,11 +644,6 @@ async function requestResend(decryptionError: DecryptionErrorEventData) { groupId: groupId ? `groupv2(${groupId})` : undefined, }); - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error(`requestResend/${logId}: messaging is not available!`); - } - // 1. Find the target conversation const sender = window.ConversationController.getOrCreate( @@ -719,7 +699,7 @@ function scheduleSessionReset(senderAci: AciString, senderDevice: number) { drop( lightSessionResetQueue.add(async () => { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); await signalProtocolStore.lightSessionReset( new QualifiedAddress(ourAci, Address.create(senderAci, senderDevice)) diff --git a/ts/util/handleServerAlerts.ts b/ts/util/handleServerAlerts.ts index 645d1d60a3..8ae2c0c6be 100644 --- a/ts/util/handleServerAlerts.ts +++ b/ts/util/handleServerAlerts.ts @@ -7,25 +7,11 @@ import { DAY, WEEK } from './durations/index.js'; import { isNotNil } from './isNotNil.js'; import { clearTimeoutIfNecessary } from './clearTimeoutIfNecessary.js'; import { safeSetTimeout } from './timeout.js'; +import { itemStorage } from '../textsecure/Storage.js'; +import { ServerAlert, type ServerAlertsType } from '../types/ServerAlert.js'; const log = createLogger('handleServerAlerts'); -export enum ServerAlert { - CRITICAL_IDLE_PRIMARY_DEVICE = 'critical_idle_primary_device', - IDLE_PRIMARY_DEVICE = 'idle_primary_device', -} - -export type ServerAlertsType = { - [ServerAlert.IDLE_PRIMARY_DEVICE]?: { - firstReceivedAt: number; - dismissedAt?: number; - }; - [ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE]?: { - firstReceivedAt: number; - modalLastDismissedAt?: number; - }; -}; - export function parseServerAlertsFromHeader( headerValue: string ): Array { @@ -51,7 +37,7 @@ export function parseServerAlertsFromHeader( export async function handleServerAlerts( receivedAlerts: Array ): Promise { - const existingAlerts = window.storage.get('serverAlerts') ?? {}; + const existingAlerts = itemStorage.get('serverAlerts') ?? {}; const existingAlertNames = new Set(Object.keys(existingAlerts)); const now = Date.now(); @@ -79,7 +65,7 @@ export async function handleServerAlerts( log.info(`removed alerts: ${[...existingAlertNames].join(', ')}`); } - await window.storage.put('serverAlerts', newAlerts); + await itemStorage.put('serverAlerts', newAlerts); } export function getServerAlertToShow( @@ -147,7 +133,7 @@ function maybeShowCriticalIdlePrimaryDeviceModal( } export async function onCriticalIdlePrimaryDeviceModalDismissed(): Promise { - const existingAlerts = window.storage.get('serverAlerts') ?? {}; + const existingAlerts = itemStorage.get('serverAlerts') ?? {}; const existingAlert = existingAlerts[ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE]; @@ -163,7 +149,7 @@ export async function onCriticalIdlePrimaryDeviceModalDismissed(): Promise modalLastDismissedAt: Date.now(), }; - await window.storage.put('serverAlerts', { + await itemStorage.put('serverAlerts', { ...existingAlerts, [ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE]: newAlert, }); diff --git a/ts/util/idForLogging.ts b/ts/util/idForLogging.ts index ceb44fe583..7cb4437465 100644 --- a/ts/util/idForLogging.ts +++ b/ts/util/idForLogging.ts @@ -9,7 +9,7 @@ import { getSource, getSourceDevice, getSourceServiceId, -} from '../messages/helpers.js'; +} from '../messages/sources.js'; import { isDirectConversation, isGroupV2 } from './whatTypeOfConversation.js'; import { getE164 } from './getE164.js'; import type { ConversationType } from '../state/ducks/conversations.js'; diff --git a/ts/util/isBackupEnabled.ts b/ts/util/isBackupEnabled.ts index e9067e38fc..302c5e96eb 100644 --- a/ts/util/isBackupEnabled.ts +++ b/ts/util/isBackupEnabled.ts @@ -5,9 +5,10 @@ import * as RemoteConfig from '../RemoteConfig.js'; import { isTestOrMockEnvironment } from '../environment.js'; import { isStagingServer } from './isStagingServer.js'; import { isBeta, isNightly } from './version.js'; +import { itemStorage } from '../textsecure/Storage.js'; export function areRemoteBackupsTurnedOn(): boolean { - return isBackupFeatureEnabled() && window.storage.get('backupTier') != null; + return isBackupFeatureEnabled() && itemStorage.get('backupTier') != null; } // Downloading from a remote backup is currently a test-only feature diff --git a/ts/util/isBlocked.ts b/ts/util/isBlocked.ts index b1f015a781..5d16995c45 100644 --- a/ts/util/isBlocked.ts +++ b/ts/util/isBlocked.ts @@ -3,21 +3,22 @@ import type { ConversationAttributesType } from '../model-types.js'; import { isAciString } from './isAciString.js'; +import { itemStorage } from '../textsecure/Storage.js'; export function isBlocked( attributes: Pick ): boolean { const { e164, groupId, serviceId } = attributes; if (isAciString(serviceId)) { - return window.storage.blocked.isServiceIdBlocked(serviceId); + return itemStorage.blocked.isServiceIdBlocked(serviceId); } if (e164) { - return window.storage.blocked.isBlocked(e164); + return itemStorage.blocked.isBlocked(e164); } if (groupId) { - return window.storage.blocked.isGroupBlocked(groupId); + return itemStorage.blocked.isGroupBlocked(groupId); } return false; diff --git a/ts/util/isConversationAccepted.ts b/ts/util/isConversationAccepted.ts index 7b291e0ee4..86b8e9de7a 100644 --- a/ts/util/isConversationAccepted.ts +++ b/ts/util/isConversationAccepted.ts @@ -6,6 +6,7 @@ import { SignalService as Proto } from '../protobuf/index.js'; import { isDirectConversation, isMe } from './whatTypeOfConversation.js'; import { isInSystemContacts } from './isInSystemContacts.js'; import { createLogger } from '../logging/log.js'; +import { itemStorage } from '../textsecure/Storage.js'; export type IsConversationAcceptedOptionsType = { ignoreEmptyConvo: boolean; @@ -46,7 +47,7 @@ export function isConversationAccepted( profileSharing, } = conversationAttrs; - const ourAci = window.storage.user.getAci(); + const ourAci = itemStorage.user.getAci(); const hasRequestedToJoin = Boolean(ourAci) && (pendingAdminApprovalV2 || []).some(item => item.aci === ourAci); diff --git a/ts/util/isMessageEmpty.ts b/ts/util/isMessageEmpty.ts index f5b4935d84..fdba842d2e 100644 --- a/ts/util/isMessageEmpty.ts +++ b/ts/util/isMessageEmpty.ts @@ -1,7 +1,7 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { messageHasPaymentEvent } from '../messages/helpers.js'; +import { messageHasPaymentEvent } from '../messages/payments.js'; import type { MessageAttributesType } from '../model-types.js'; import { hasErrors, diff --git a/ts/util/lookupConversationWithoutServiceId.ts b/ts/util/lookupConversationWithoutServiceId.ts index 2d53557f0f..ea18d0fae0 100644 --- a/ts/util/lookupConversationWithoutServiceId.ts +++ b/ts/util/lookupConversationWithoutServiceId.ts @@ -6,6 +6,7 @@ import { usernames, LibSignalErrorBase } from '@signalapp/libsignal-client'; import type { UserNotFoundModalStateType } from '../state/ducks/globalModals.js'; import { createLogger } from '../logging/log.js'; import type { AciString } from '../types/ServiceId.js'; +import { getAccountForUsername, cdsLookup } from '../textsecure/WebAPI.js'; import * as Errors from '../types/errors.js'; import { ToastType } from '../types/Toast.js'; import { HTTPError } from '../types/HTTPError.js'; @@ -65,16 +66,11 @@ export async function lookupConversationWithoutServiceId( const { showUserNotFoundModal, setIsFetchingUUID } = options; setIsFetchingUUID(identifier, true); - const { server } = window.textsecure; - if (!server) { - throw new Error('server is not available!'); - } - try { let conversationId: string | undefined; if (options.type === 'e164') { const { entries: serverLookup, transformedE164s } = - await getServiceIdsForE164s(server, [options.e164]); + await getServiceIdsForE164s(cdsLookup, [options.e164]); const e164ToUse = transformedE164s.get(options.e164) ?? options.e164; const maybePair = serverLookup.get(e164ToUse); @@ -156,13 +152,8 @@ export async function checkForUsername( return undefined; } - const { server } = window.textsecure; - if (!server) { - throw new Error('server is not available!'); - } - try { - const account = await server.getAccountForUsername({ + const account = await getAccountForUsername({ hash, }); diff --git a/ts/util/makeQuote.ts b/ts/util/makeQuote.ts index 22c6d54a23..2fb8d63ff9 100644 --- a/ts/util/makeQuote.ts +++ b/ts/util/makeQuote.ts @@ -9,7 +9,7 @@ import type { import type { LinkPreviewType } from '../types/message/LinkPreviews.js'; import type { StickerType } from '../types/Stickers.js'; import { IMAGE_JPEG, IMAGE_GIF } from '../types/MIME.js'; -import { getAuthor } from '../messages/helpers.js'; +import { getAuthor } from '../messages/sources.js'; import { getQuoteBodyText } from './getQuoteBodyText.js'; import { isGIF } from './Attachment.js'; import { isGiftBadge, isTapToView } from '../state/selectors/message.js'; diff --git a/ts/util/markConversationRead.ts b/ts/util/markConversationRead.ts index 3a03e61e8e..53449e6e01 100644 --- a/ts/util/markConversationRead.ts +++ b/ts/util/markConversationRead.ts @@ -27,6 +27,7 @@ import type { AciString } from '../types/ServiceId.js'; import { isAciString } from './isAciString.js'; import type { MessageModel } from '../models/messages.js'; import { postSaveUpdates } from './cleanup.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { isNumber, pick } = lodash; @@ -172,7 +173,7 @@ export async function markConversationRead( await DataWriter.saveMessages( updatedMessages.map(msg => msg.attributes), { - ourAci: window.textsecure.storage.user.getCheckedAci(), + ourAci: itemStorage.user.getCheckedAci(), postSaveUpdates, } ); diff --git a/ts/util/markOnboardingStoryAsRead.ts b/ts/util/markOnboardingStoryAsRead.ts index 4c2476b685..6333e1e12a 100644 --- a/ts/util/markOnboardingStoryAsRead.ts +++ b/ts/util/markOnboardingStoryAsRead.ts @@ -9,11 +9,12 @@ import { DurationInSeconds } from './durations/index.js'; import { markViewed } from '../services/MessageUpdater.js'; import { storageServiceUploadJob } from '../services/storage.js'; import { postSaveUpdates } from './cleanup.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('markOnboardingStoryAsRead'); export async function markOnboardingStoryAsRead(): Promise { - const existingOnboardingStoryMessageIds = window.storage.get( + const existingOnboardingStoryMessageIds = itemStorage.get( 'existingOnboardingStoryMessageIds' ); @@ -47,11 +48,11 @@ export async function markOnboardingStoryAsRead(): Promise { log.info(`marked ${messageAttributes.length} viewed`); await DataWriter.saveMessages(messageAttributes, { - ourAci: window.textsecure.storage.user.getCheckedAci(), + ourAci: itemStorage.user.getCheckedAci(), postSaveUpdates, }); - await window.storage.put('hasViewedOnboardingStory', true); + await itemStorage.put('hasViewedOnboardingStory', true); storageServiceUploadJob({ reason: 'markOnboardingStoryAsRead' }); diff --git a/ts/util/messageBatcher.ts b/ts/util/messageBatcher.ts index 82fb51593a..6ac938a37a 100644 --- a/ts/util/messageBatcher.ts +++ b/ts/util/messageBatcher.ts @@ -7,6 +7,7 @@ import { DataWriter } from '../sql/Client.js'; import { createLogger } from '../logging/log.js'; import { postSaveUpdates } from './cleanup.js'; import { MessageModel } from '../models/messages.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('messageBatcher'); @@ -23,7 +24,7 @@ const updateMessageBatcher = createWaitBatcher({ ); await DataWriter.saveMessages(messagesToSave, { - ourAci: window.textsecure.storage.user.getCheckedAci(), + ourAci: itemStorage.user.getCheckedAci(), postSaveUpdates, }); }, @@ -64,7 +65,7 @@ export const saveNewMessageBatcher = await DataWriter.saveMessages(messagesToSave, { forceSave: true, - ourAci: window.textsecure.storage.user.getCheckedAci(), + ourAci: itemStorage.user.getCheckedAci(), postSaveUpdates, }); }, diff --git a/ts/util/modifyTargetMessage.ts b/ts/util/modifyTargetMessage.ts index 9c2b841610..440a36af6b 100644 --- a/ts/util/modifyTargetMessage.ts +++ b/ts/util/modifyTargetMessage.ts @@ -29,7 +29,7 @@ import { handleEditMessage } from './handleEditMessage.js'; import { isGroup } from './whatTypeOfConversation.js'; import { isStory, isTapToView } from '../state/selectors/message.js'; import { getOwn } from './getOwn.js'; -import { getSourceServiceId } from '../messages/helpers.js'; +import { getSourceServiceId } from '../messages/sources.js'; import { missingCaseError } from './missingCaseError.js'; import { reduce } from './iterables.js'; import { strictAssert } from './assert.js'; @@ -46,6 +46,7 @@ import { handlePollTerminate, handlePollVote, } from '../messageModifiers/Polls.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { isEqual } = lodash; @@ -70,7 +71,7 @@ export async function modifyTargetMessage( const logId = `modifyTargetMessage/${getMessageIdForLogging(message.attributes)}`; const type = message.get('type'); let changed = false; - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const sourceServiceId = getSourceServiceId(message.attributes); const syncDeletes = await DeletesForMe.forMessage(message.attributes); diff --git a/ts/util/onDeviceNameChangeSync.ts b/ts/util/onDeviceNameChangeSync.ts index 58964ec5e1..8b6f343a70 100644 --- a/ts/util/onDeviceNameChangeSync.ts +++ b/ts/util/onDeviceNameChangeSync.ts @@ -3,12 +3,14 @@ import PQueue from 'p-queue'; import type { DeviceNameChangeSyncEvent } from '../textsecure/messageReceiverEvents.js'; +import { getDevices } from '../textsecure/WebAPI.js'; import { MINUTE } from './durations/index.js'; import { strictAssert } from './assert.js'; import { parseIntOrThrow } from './parseIntOrThrow.js'; import { createLogger } from '../logging/log.js'; import { toLogFormat } from '../types/errors.js'; import { drop } from './drop.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('onDeviceNameChangeSync'); @@ -47,10 +49,9 @@ export async function maybeQueueDeviceNameFetch(): Promise { } async function fetchAndUpdateDeviceName() { - strictAssert(window.textsecure.server, 'WebAPI must be initialized'); - const { devices } = await window.textsecure.server.getDevices(); + const { devices } = await getDevices(); const localDeviceId = parseIntOrThrow( - window.textsecure.storage.user.getDeviceId(), + itemStorage.user.getDeviceId(), 'fetchAndUpdateDeviceName: localDeviceId' ); const ourDevice = devices.find(device => device.id === localDeviceId); @@ -69,21 +70,20 @@ async function fetchAndUpdateDeviceName() { .getAccountManager() .decryptDeviceName(newNameEncrypted); } catch (e) { - const deviceNameWasEncrypted = - window.textsecure.storage.user.getDeviceNameEncrypted(); + const deviceNameWasEncrypted = itemStorage.user.getDeviceNameEncrypted(); log.error( `fetchAndUpdateDeviceName: failed to decrypt device name. Was encrypted local state: ${deviceNameWasEncrypted}` ); return; } - const existingName = window.storage.user.getDeviceName(); + const existingName = itemStorage.user.getDeviceName(); if (newName === existingName) { log.info('fetchAndUpdateDeviceName: new name matches existing name'); return; } - await window.storage.user.setDeviceName(newName); + await itemStorage.user.setDeviceName(newName); window.Whisper.events.emit('deviceNameChanged'); log.info( 'fetchAndUpdateDeviceName: successfully updated new device name locally' diff --git a/ts/util/phoneNumberSharingMode.ts b/ts/util/phoneNumberSharingMode.ts index 43f532550a..50c4000768 100644 --- a/ts/util/phoneNumberSharingMode.ts +++ b/ts/util/phoneNumberSharingMode.ts @@ -9,10 +9,11 @@ import { } from '../types/PhoneNumberSharingMode.js'; import { missingCaseError } from './missingCaseError.js'; import { isDirectConversation, isMe } from './whatTypeOfConversation.js'; +import { itemStorage } from '../textsecure/Storage.js'; export const isSharingPhoneNumberWithEverybody = (): boolean => { const phoneNumberSharingMode = parsePhoneNumberSharingMode( - window.storage.get('phoneNumberSharingMode') + itemStorage.get('phoneNumberSharingMode') ); switch (phoneNumberSharingMode) { diff --git a/ts/util/queueAttachmentDownloads.ts b/ts/util/queueAttachmentDownloads.ts index a51ef213e4..6b30309a5c 100644 --- a/ts/util/queueAttachmentDownloads.ts +++ b/ts/util/queueAttachmentDownloads.ts @@ -46,7 +46,10 @@ import { } from './attachmentDownloadQueue.js'; import { queueUpdateMessage } from './messageBatcher.js'; import type { LoggerType } from '../types/Logging.js'; -import { DEFAULT_AUTO_DOWNLOAD_ATTACHMENT } from '../textsecure/Storage.js'; +import { + itemStorage, + DEFAULT_AUTO_DOWNLOAD_ATTACHMENT, +} from '../textsecure/Storage.js'; const defaultLogger = createLogger('queueAttachmentDownloads'); @@ -143,7 +146,7 @@ export async function queueAttachmentDownloads( ); } - const autoDownloadAttachment = window.storage.get( + const autoDownloadAttachment = itemStorage.get( 'auto-download-attachment', DEFAULT_AUTO_DOWNLOAD_ATTACHMENT ); @@ -549,7 +552,7 @@ export async function queueNormalAttachments({ const { contentType } = attachment; if (!isManualDownload) { - const autoDownloadAttachment = window.storage.get( + const autoDownloadAttachment = itemStorage.get( 'auto-download-attachment', DEFAULT_AUTO_DOWNLOAD_ATTACHMENT ); @@ -664,7 +667,7 @@ async function queuePreviews({ } if (!isManualDownload) { - const autoDownloadAttachment = window.storage.get( + const autoDownloadAttachment = itemStorage.get( 'auto-download-attachment', DEFAULT_AUTO_DOWNLOAD_ATTACHMENT ); diff --git a/ts/util/registration.ts b/ts/util/registration.ts index cd62637dab..d820c2c1c0 100644 --- a/ts/util/registration.ts +++ b/ts/util/registration.ts @@ -1,23 +1,24 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { itemStorage } from '../textsecure/Storage.js'; export async function markEverDone(): Promise { - await window.storage.put('chromiumRegistrationDoneEver', ''); + await itemStorage.put('chromiumRegistrationDoneEver', ''); } export async function markDone(): Promise { await markEverDone(); - await window.storage.put('chromiumRegistrationDone', ''); + await itemStorage.put('chromiumRegistrationDone', ''); } export async function remove(): Promise { - await window.storage.remove('chromiumRegistrationDone'); + await itemStorage.remove('chromiumRegistrationDone'); } export function isDone(): boolean { - return window.storage.get('chromiumRegistrationDone') === ''; + return itemStorage.get('chromiumRegistrationDone') === ''; } export function everDone(): boolean { - return window.storage.get('chromiumRegistrationDoneEver') === '' || isDone(); + return itemStorage.get('chromiumRegistrationDoneEver') === '' || isDone(); } diff --git a/ts/util/safetyNumber.ts b/ts/util/safetyNumber.ts index 2839c3f884..3cd0ba91d6 100644 --- a/ts/util/safetyNumber.ts +++ b/ts/util/safetyNumber.ts @@ -10,6 +10,7 @@ import { createLogger } from '../logging/log.js'; import type { SafetyNumberType } from '../types/safetyNumber.js'; import { signalProtocolStore } from '../SignalProtocolStore.js'; import { isAciString } from './isAciString.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('safetyNumber'); @@ -25,8 +26,7 @@ export async function generateSafetyNumber( const logId = `generateSafetyNumbers(${contact.id})`; log.info(`${logId}: starting`); - const { storage } = window.textsecure; - const ourAci = storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const us = signalProtocolStore.getIdentityRecord(ourAci); const ourKeyBuffer = us ? us.publicKey : null; diff --git a/ts/util/scaleImageToLevel.ts b/ts/util/scaleImageToLevel.ts index e4fdbd8623..b2a9266e8a 100644 --- a/ts/util/scaleImageToLevel.ts +++ b/ts/util/scaleImageToLevel.ts @@ -9,6 +9,7 @@ import { IMAGE_JPEG } from '../types/MIME.js'; import { canvasToBlob } from './canvasToBlob.js'; import { getValue } from '../RemoteConfig.js'; import { parseNumber } from './libphonenumberUtil.js'; +import { itemStorage } from '../textsecure/Storage.js'; enum MediaQualityLevels { One = 1, @@ -70,7 +71,7 @@ function getMediaQualityLevel(): MediaQualityLevels { return DEFAULT_LEVEL; } - const e164 = window.textsecure.storage.user.getNumber(); + const e164 = itemStorage.user.getNumber(); if (!e164) { return DEFAULT_LEVEL; } diff --git a/ts/util/sendCallLinkUpdateSync.ts b/ts/util/sendCallLinkUpdateSync.ts index def4da7853..60f7126188 100644 --- a/ts/util/sendCallLinkUpdateSync.ts +++ b/ts/util/sendCallLinkUpdateSync.ts @@ -9,9 +9,10 @@ import { createLogger } from '../logging/log.js'; import * as Errors from '../types/errors.js'; import { SignalService as Proto } from '../protobuf/index.js'; import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue.js'; -import MessageSender from '../textsecure/SendMessage.js'; +import { MessageSender } from '../textsecure/SendMessage.js'; import { toAdminKeyBytes } from './callLinks.js'; import { toEpochBytes, toRootKeyBytes } from './callLinksRingrtc.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('sendCallLinkUpdateSync'); @@ -41,7 +42,7 @@ async function _sendCallLinkUpdateSync( log.info(`Sending CallLinkUpdate type=${type}`); try { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const callLinkUpdate = new Proto.SyncMessage.CallLinkUpdate({ type: protoType, diff --git a/ts/util/sendEditedMessage.ts b/ts/util/sendEditedMessage.ts index 206905f3f1..a75256e7c6 100644 --- a/ts/util/sendEditedMessage.ts +++ b/ts/util/sendEditedMessage.ts @@ -23,7 +23,7 @@ import { } from '../jobs/conversationJobQueue.js'; import { concat, filter, map, repeat, zipObject, find } from './iterables.js'; import { getConversationIdForLogging } from './idForLogging.js'; -import { isQuoteAMatch } from '../messages/helpers.js'; +import { isQuoteAMatch } from '../messages/quotes.js'; import { getMessageById } from '../messages/getMessageById.js'; import { handleEditMessage } from './handleEditMessage.js'; import { incrementMessageCounter } from './incrementMessageCounter.js'; @@ -34,6 +34,7 @@ import { strictAssert } from './assert.js'; import { timeAndLogIfTooLong } from './timeAndLogIfTooLong.js'; import { makeQuote } from './makeQuote.js'; import { getMessageSentTimestamp } from './getMessageSentTimestamp.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('sendEditedMessage'); @@ -57,9 +58,6 @@ export async function sendEditedMessage( targetMessageId: string; } ): Promise { - const { messaging } = window.textsecure; - strictAssert(messaging, 'messaging not available'); - const conversation = window.ConversationController.get(conversationId); strictAssert(conversation, 'no conversation found'); @@ -202,7 +200,7 @@ export async function sendEditedMessage( await handleEditMessage(targetMessage.attributes, { conversationId, fromId, - fromDevice: window.storage.user.getDeviceId() ?? 1, + fromDevice: itemStorage.user.getDeviceId() ?? 1, message: tmpMessage, }); diff --git a/ts/util/sendReceipts.ts b/ts/util/sendReceipts.ts index 2f66d5dc48..0d1ca96883 100644 --- a/ts/util/sendReceipts.ts +++ b/ts/util/sendReceipts.ts @@ -13,6 +13,8 @@ import { missingCaseError } from './missingCaseError.js'; import type { ConversationModel } from '../models/conversations.js'; import { mapEmplace } from './mapEmplace.js'; import { isSignalConversation } from './isSignalConversation.js'; +import { messageSender } from '../textsecure/SendMessage.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { chunk, map } = lodash; @@ -49,12 +51,7 @@ export async function sendReceipts({ throw missingCaseError(type); } - const { messaging } = window.textsecure; - if (!messaging) { - throw new Error('messaging is not available!'); - } - - if (requiresUserSetting && !window.storage.get('read-receipt-setting')) { + if (requiresUserSetting && !itemStorage.get('read-receipt-setting')) { log.info('requires user setting. Not sending these receipts'); return; } @@ -154,7 +151,7 @@ export async function sendReceipts({ const senderAci = sender.getCheckedAci('sendReceipts'); await handleMessageSend( - messaging[methodName]({ + messageSender[methodName]({ senderAci, isDirectConversation, timestamps, diff --git a/ts/util/sendStoryMessage.ts b/ts/util/sendStoryMessage.ts index 0d1b90b7bd..ca3362b11e 100644 --- a/ts/util/sendStoryMessage.ts +++ b/ts/util/sendStoryMessage.ts @@ -32,6 +32,7 @@ import { DurationInSeconds } from './durations/index.js'; import { sanitizeLinkPreview } from '../services/LinkPreview.js'; import type { DraftBodyRanges } from '../types/BodyRange.js'; import { MessageModel } from '../models/messages.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('sendStoryMessage'); @@ -46,15 +47,6 @@ export async function sendStoryMessage( return; } - const { messaging } = window.textsecure; - - if (!messaging) { - log.warn( - 'stories.sendStoryMessage: messaging not available, returning early' - ); - return; - } - const distributionLists = ( await Promise.all( listIds.map(listId => DataReader.getStoryDistributionWithMembers(listId)) @@ -188,9 +180,9 @@ export async function sendStoryMessage( seenStatus: SeenStatus.NotApplicable, sendStateByConversationId, sent_at: timestamp, - source: window.textsecure.storage.user.getNumber(), - sourceServiceId: window.textsecure.storage.user.getAci(), - sourceDevice: window.textsecure.storage.user.getDeviceId(), + source: itemStorage.user.getNumber(), + sourceServiceId: itemStorage.user.getAci(), + sourceDevice: itemStorage.user.getDeviceId(), storyDistributionListId: distributionList.id, timestamp, type: 'story', @@ -295,9 +287,9 @@ export async function sendStoryMessage( seenStatus: SeenStatus.NotApplicable, sendStateByConversationId, sent_at: groupTimestamp, - source: window.textsecure.storage.user.getNumber(), - sourceServiceId: window.textsecure.storage.user.getAci(), - sourceDevice: window.textsecure.storage.user.getDeviceId(), + source: itemStorage.user.getNumber(), + sourceServiceId: itemStorage.user.getAci(), + sourceDevice: itemStorage.user.getDeviceId(), timestamp: groupTimestamp, type: 'story', }); diff --git a/ts/util/sendToGroup.ts b/ts/util/sendToGroup.ts index 46f7c45582..6824553236 100644 --- a/ts/util/sendToGroup.ts +++ b/ts/util/sendToGroup.ts @@ -36,6 +36,7 @@ import type { GroupSendOptionsType, SendOptionsType, } from '../textsecure/SendMessage.js'; +import { messageSender } from '../textsecure/SendMessage.js'; import { ConnectTimeoutError, IncorrectSenderKeyAuthError, @@ -58,6 +59,9 @@ import { SEALED_SENDER, ZERO_ACCESS_KEY } from '../types/SealedSender.js'; import { HTTPError } from '../types/HTTPError.js'; import { parseIntOrThrow } from './parseIntOrThrow.js'; import { + sendWithSenderKey, + getKeysForServiceId as doGetKeysForServiceId, + getKeysForServiceIdUnauth as doGetKeysForServiceIdUnauth, multiRecipient200ResponseSchema, multiRecipient409ResponseSchema, multiRecipient410ResponseSchema, @@ -75,6 +79,7 @@ import { import type { GroupSendToken } from '../types/GroupSendEndorsements.js'; import { isAciString } from './isAciString.js'; import { safeParseStrict, safeParseUnknown } from './schemas.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { differenceWith, omit } = lodash; @@ -130,19 +135,13 @@ export async function sendToGroup({ story?: boolean; urgent: boolean; }): Promise { - strictAssert( - window.textsecure.messaging, - 'sendToGroup: textsecure.messaging not available!' - ); - const { timestamp } = groupSendOptions; const recipients = getRecipients(groupSendOptions); // First, do the attachment upload and prepare the proto we'll be sending const protoAttributes = - window.textsecure.messaging.getAttrsFromGroupOptions(groupSendOptions); - const contentMessage = - await window.textsecure.messaging.getContentMessage(protoAttributes); + messageSender.getAttrsFromGroupOptions(groupSendOptions); + const contentMessage = await messageSender.getContentMessage(protoAttributes); // Attachment upload might take too long to succeed - we don't want to proceed // with the send if the caller aborted this call. @@ -208,11 +207,6 @@ export async function sendContentMessageToGroup( } } - strictAssert( - window.textsecure.messaging, - 'sendContentMessageToGroup: textsecure.messaging not available!' - ); - if (sendTarget.isValid()) { try { return await sendToGroupViaSenderKey(options, { @@ -236,7 +230,7 @@ export async function sendContentMessageToGroup( } } - const sendLogCallback = window.textsecure.messaging.makeSendLogCallback({ + const sendLogCallback = messageSender.makeSendLogCallback({ contentHint, messageId, proto: Proto.Content.encode(contentMessage).finish(), @@ -246,7 +240,7 @@ export async function sendContentMessageToGroup( hasPniSignatureMessage: false, }); const groupId = sendTarget.isGroupV2() ? sendTarget.getGroupId() : undefined; - return window.textsecure.messaging.sendGroupProto({ + return messageSender.sendGroupProto({ contentHint, groupId, options: { ...sendOptions, online }, @@ -321,11 +315,6 @@ export async function sendToGroupViaSenderKey( throw new Error(`${logId}: Invalid contentHint ${contentHint}`); } - strictAssert( - window.textsecure.messaging, - 'sendToGroupViaSenderKey: textsecure.messaging not available!' - ); - // 1. Add sender key info if we have none, or clear out if it's too old // Note: From here on, generally need to recurse if we change senderKeyInfo const senderKeyInfo = sendTarget.getSenderKeyInfo(); @@ -353,7 +342,7 @@ export async function sendToGroupViaSenderKey( } // 2. Fetch all devices we believe we'll be sending to - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); const { devices: currentDevices, emptyServiceIds } = await signalProtocolStore.getOpenDevices(ourAci, recipients); @@ -459,7 +448,7 @@ export async function sendToGroupViaSenderKey( ); try { await handleMessageSend( - window.textsecure.messaging.sendSenderKeyDistributionMessage( + messageSender.sendSenderKeyDistributionMessage( { contentHint, distributionId, @@ -551,7 +540,7 @@ export async function sendToGroupViaSenderKey( groupId, }); - const result = await window.textsecure.messaging.server.sendWithSenderKey( + const result = await sendWithSenderKey( messageBuffer, accessKeys, groupSendToken, @@ -715,7 +704,7 @@ export async function sendToGroupViaSenderKey( }; try { - const normalSendResult = await window.textsecure.messaging.sendGroupProto({ + const normalSendResult = await messageSender.sendGroupProto({ contentHint, groupId, options: { ...sendOptions, online }, @@ -769,7 +758,7 @@ export async function resetSenderKey( memberDevices: [], }); - const ourAci = window.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); await signalProtocolStore.removeSenderKey( new QualifiedAddress(ourAci, ourAddress), distributionId @@ -988,7 +977,7 @@ async function handle409Response( // Archive sessions with devices that have been removed if (devices.extraDevices && devices.extraDevices.length > 0) { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); await waitForAll({ tasks: devices.extraDevices.map(deviceId => async () => { @@ -1027,7 +1016,7 @@ async function handle410Response( tasks: parsed.data.map(item => async () => { const { uuid, devices } = item; if (devices.staleDevices && devices.staleDevices.length > 0) { - const ourAci = window.textsecure.storage.user.getCheckedAci(); + const ourAci = itemStorage.user.getCheckedAci(); // First, archive our existing sessions with these devices await waitForAll({ @@ -1132,8 +1121,8 @@ async function encryptForSenderKey({ distributionId: string; groupId?: string; }): Promise { - const ourAci = window.textsecure.storage.user.getCheckedAci(); - const ourDeviceId = window.textsecure.storage.user.getDeviceId(); + const ourAci = itemStorage.user.getCheckedAci(); + const ourDeviceId = itemStorage.user.getDeviceId(); if (!ourDeviceId) { throw new Error( 'encryptForSenderKey: Unable to fetch our uuid or deviceId' @@ -1314,8 +1303,8 @@ export function _analyzeSenderKeyDevices( } function getOurAddress(): Address { - const ourAci = window.textsecure.storage.user.getCheckedAci(); - const ourDeviceId = window.textsecure.storage.user.getDeviceId(); + const ourAci = itemStorage.user.getCheckedAci(); + const ourDeviceId = itemStorage.user.getDeviceId(); if (!ourDeviceId) { throw new Error('getOurAddress: Unable to fetch our deviceId'); } @@ -1379,10 +1368,6 @@ async function fetchKeysForServiceId( const logId = `fetchKeysForServiceId/${serviceId}`; log.info(`${logId}: Fetching ${devices || 'all'} devices`); - if (!window.textsecure?.messaging?.server) { - throw new Error('fetchKeysForServiceId: No server available!'); - } - const emptyConversation = window.ConversationController.getOrCreate( serviceId, 'private' @@ -1411,7 +1396,10 @@ async function fetchKeysForServiceId( const { accessKeyFailed } = await getKeysForServiceId( serviceId, - window.textsecure?.messaging?.server, + { + getKeysForServiceId: doGetKeysForServiceId, + getKeysForServiceIdUnauth: doGetKeysForServiceIdUnauth, + }, devices, accessKey, groupSendToken diff --git a/ts/util/shouldShowInvalidMessageToast.ts b/ts/util/shouldShowInvalidMessageToast.ts index 4f5a83d416..4edc9938ae 100644 --- a/ts/util/shouldShowInvalidMessageToast.ts +++ b/ts/util/shouldShowInvalidMessageToast.ts @@ -12,6 +12,7 @@ import { isGroupV1, isGroupV2, } from './whatTypeOfConversation.js'; +import { itemStorage } from '../textsecure/Storage.js'; const MAX_MESSAGE_BODY_LENGTH = 64 * 1024; @@ -39,8 +40,8 @@ export function shouldShowInvalidMessageToast( const { e164, serviceId } = conversationAttributes; if ( isDirectConversation(conversationAttributes) && - ((e164 && window.storage.blocked.isBlocked(e164)) || - (serviceId && window.storage.blocked.isServiceIdBlocked(serviceId))) + ((e164 && itemStorage.blocked.isBlocked(e164)) || + (serviceId && itemStorage.blocked.isServiceIdBlocked(serviceId))) ) { return { toastType: ToastType.Blocked }; } @@ -49,7 +50,7 @@ export function shouldShowInvalidMessageToast( if ( !isDirectConversation(conversationAttributes) && groupId && - window.storage.blocked.isGroupBlocked(groupId) + itemStorage.blocked.isGroupBlocked(groupId) ) { return { toastType: ToastType.BlockedGroup }; } diff --git a/ts/util/shouldStoryReplyNotifyUser.ts b/ts/util/shouldStoryReplyNotifyUser.ts index f49b752743..b83176aae9 100644 --- a/ts/util/shouldStoryReplyNotifyUser.ts +++ b/ts/util/shouldStoryReplyNotifyUser.ts @@ -7,6 +7,7 @@ import { createLogger } from '../logging/log.js'; import { DataReader } from '../sql/Client.js'; import { isGroup } from './whatTypeOfConversation.js'; import { isMessageUnread } from './isMessageUnread.js'; +import { itemStorage } from '../textsecure/Storage.js'; const log = createLogger('shouldStoryReplyNotifyUser'); @@ -47,7 +48,7 @@ export async function shouldStoryReplyNotifyUser( return false; } - const ourAci = window.textsecure.storage.user.getAci(); + const ourAci = itemStorage.user.getAci(); const storySourceAci = matchedStory.sourceServiceId; const currentUserIdSource = storySourceAci === ourAci; diff --git a/ts/util/stories.ts b/ts/util/stories.ts index af60a96da6..cf323b12c1 100644 --- a/ts/util/stories.ts +++ b/ts/util/stories.ts @@ -1,14 +1,16 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { itemStorage } from '../textsecure/Storage.js'; +import { onHasStoriesDisabledChange } from '../textsecure/WebAPI.js'; export const getStoriesDisabled = (): boolean => - window.storage.get('hasStoriesDisabled', false); + itemStorage.get('hasStoriesDisabled', false); export const setStoriesDisabled = async (value: boolean): Promise => { - await window.storage.put('hasStoriesDisabled', value); + await itemStorage.put('hasStoriesDisabled', value); const account = window.ConversationController.getOurConversationOrThrow(); account.captureChange('hasStoriesDisabled'); - window.textsecure.server?.onHasStoriesDisabledChange(value); + onHasStoriesDisabledChange(value); }; export const getStoriesBlocked = (): boolean => getStoriesDisabled(); diff --git a/ts/util/subscriptionConfiguration.ts b/ts/util/subscriptionConfiguration.ts index 134d3102bd..0696db27e2 100644 --- a/ts/util/subscriptionConfiguration.ts +++ b/ts/util/subscriptionConfiguration.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { SubscriptionConfigurationResultType } from '../textsecure/WebAPI.js'; +import { getSubscriptionConfiguration } from '../textsecure/WebAPI.js'; import type { OneTimeDonationHumanAmounts } from '../types/Donations.js'; import { HOUR } from './durations/index.js'; import { isInPast } from './timestamp.js'; @@ -23,12 +24,7 @@ export async function getCachedSubscriptionConfiguration(): Promise { - return window.storage.put(ITEM_NAME, newValue || DurationInSeconds.ZERO); + return itemStorage.put(ITEM_NAME, newValue || DurationInSeconds.ZERO); } diff --git a/ts/util/updateBackupMediaDownloadProgress.ts b/ts/util/updateBackupMediaDownloadProgress.ts index 0c23f4191c..26306b25fd 100644 --- a/ts/util/updateBackupMediaDownloadProgress.ts +++ b/ts/util/updateBackupMediaDownloadProgress.ts @@ -3,6 +3,7 @@ import lodash from 'lodash'; import type { BackupAttachmentDownloadProgress } from '../sql/Interface.js'; +import { itemStorage } from '../textsecure/Storage.js'; const { throttle } = lodash; @@ -13,8 +14,8 @@ export async function updateBackupMediaDownloadProgress( await getBackupAttachmentDownloadProgress(); await Promise.all([ - window.storage.put('backupMediaDownloadCompletedBytes', completedBytes), - window.storage.put('backupMediaDownloadTotalBytes', totalBytes), + itemStorage.put('backupMediaDownloadCompletedBytes', completedBytes), + itemStorage.put('backupMediaDownloadTotalBytes', totalBytes), ]); } diff --git a/ts/util/uploadAttachment.ts b/ts/util/uploadAttachment.ts index 4f986e9d06..3df76af89c 100644 --- a/ts/util/uploadAttachment.ts +++ b/ts/util/uploadAttachment.ts @@ -8,11 +8,15 @@ import type { } from '../types/Attachment.js'; import { MIMETypeToString, supportsIncrementalMac } from '../types/MIME.js'; import { getRandomBytes } from '../Crypto.js'; -import { strictAssert } from './assert.js'; import { backupsService } from '../services/backups/index.js'; import { tusUpload } from './uploads/tusProtocol.js'; import { defaultFileReader } from './uploads/uploads.js'; -import type { AttachmentUploadFormResponseType } from '../textsecure/WebAPI.js'; +import { + type AttachmentUploadFormResponseType, + getAttachmentUploadForm, + createFetchForAttachmentUpload, + putEncryptedAttachment, +} from '../textsecure/WebAPI.js'; import { type EncryptedAttachmentV2, encryptAttachmentV2ToDisk, @@ -28,9 +32,6 @@ const CDNS_SUPPORTING_TUS = new Set([3]); export async function uploadAttachment( attachment: AttachmentWithHydratedData ): Promise { - const { server } = window.textsecure; - strictAssert(server, 'WebAPI must be initialized'); - const keys = getRandomBytes(64); const needIncrementalMac = supportsIncrementalMac(attachment.contentType); @@ -86,16 +87,13 @@ export async function encryptAndUploadAttachment({ cdnNumber: number; encrypted: EncryptedAttachmentV2; }> { - const { server } = window.textsecure; - strictAssert(server, 'WebAPI must be initialized'); - let uploadForm: AttachmentUploadFormResponseType; let absoluteCiphertextPath: string | undefined; try { switch (uploadType) { case 'standard': - uploadForm = await server.getAttachmentUploadForm(); + uploadForm = await getAttachmentUploadForm(); break; case 'backup': uploadForm = await backupsService.api.getMediaUploadForm(); @@ -139,11 +137,8 @@ export async function uploadFile({ ciphertextFileSize: number; uploadForm: AttachmentUploadFormResponseType; }): Promise { - const { server } = window.textsecure; - strictAssert(server, 'WebAPI must be initialized'); - if (CDNS_SUPPORTING_TUS.has(uploadForm.cdn)) { - const fetchFn = server.createFetchForAttachmentUpload(uploadForm); + const fetchFn = createFetchForAttachmentUpload(uploadForm); await tusUpload({ endpoint: uploadForm.signedUploadLocation, // the upload form headers are already included in the created fetch function @@ -155,7 +150,7 @@ export async function uploadFile({ fetchFn, }); } else { - await server.putEncryptedAttachment( + await putEncryptedAttachment( (start, end) => createReadStream(absoluteCiphertextPath, { start, end }), ciphertextFileSize, uploadForm diff --git a/ts/util/verifyStoryListMembers.ts b/ts/util/verifyStoryListMembers.ts index fd990f30ee..4bfd17d3f5 100644 --- a/ts/util/verifyStoryListMembers.ts +++ b/ts/util/verifyStoryListMembers.ts @@ -6,6 +6,7 @@ import { isNotNil } from './isNotNil.js'; import { updateIdentityKey } from '../services/profiles.js'; import type { ServiceIdString } from '../types/ServiceId.js'; import { signalProtocolStore } from '../SignalProtocolStore.js'; +import { postBatchIdentityCheck } from '../textsecure/WebAPI.js'; import * as Bytes from '../Bytes.js'; const log = createLogger('verifyStoryListMembers'); @@ -16,11 +17,6 @@ export async function verifyStoryListMembers( untrustedServiceIds: Set; verifiedServiceIds: Set; }> { - const { server } = window.textsecure; - if (!server) { - throw new Error('verifyStoryListMembers: server not available'); - } - const verifiedServiceIds = new Set(); const untrustedServiceIds = new Set(); @@ -39,7 +35,7 @@ export async function verifyStoryListMembers( }) ); - const { elements: unverifiedServiceId } = await server.postBatchIdentityCheck( + const { elements: unverifiedServiceId } = await postBatchIdentityCheck( elements.filter(isNotNil) ); diff --git a/ts/util/waitBatcher.ts b/ts/util/waitBatcher.ts index 1debdf2b31..da604aee96 100644 --- a/ts/util/waitBatcher.ts +++ b/ts/util/waitBatcher.ts @@ -13,23 +13,12 @@ import { explodePromise } from './explodePromise.js'; const log = createLogger('waitBatcher'); -declare global { - // We want to extend `window`'s properties, so we need an interface. - // eslint-disable-next-line no-restricted-syntax - interface Window { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - waitBatchers: Array>; - waitForAllWaitBatchers: () => Promise; - flushAllWaitBatchers: () => Promise; - } -} +let waitBatchers = new Array>(); -window.waitBatchers = []; - -window.flushAllWaitBatchers = async () => { +export const flushAllWaitBatchers = async (): Promise => { log.info('flushAllWaitBatchers'); try { - await Promise.all(window.waitBatchers.map(item => item.flushAndWait())); + await Promise.all(waitBatchers.map(item => item.flushAndWait())); } catch (error) { log.error( 'flushAllWaitBatchers: Error flushing all', @@ -38,10 +27,10 @@ window.flushAllWaitBatchers = async () => { } }; -window.waitForAllWaitBatchers = async () => { +export const waitForAllWaitBatchers = async (): Promise => { log.info('waitForAllWaitBatchers'); try { - await Promise.all(window.waitBatchers.map(item => item.onIdle())); + await Promise.all(waitBatchers.map(item => item.onIdle())); } catch (error) { log.error( 'waitForAllWaitBatchers: Error waiting for all', @@ -155,9 +144,7 @@ export function createWaitBatcher( } function unregister() { - window.waitBatchers = window.waitBatchers.filter( - item => item !== waitBatcher - ); + waitBatchers = waitBatchers.filter(item => item !== waitBatcher); } // Meant for a full shutdown of the queue @@ -208,7 +195,7 @@ export function createWaitBatcher( pushNoopAndWait, }; - window.waitBatchers.push(waitBatcher); + waitBatchers.push(waitBatcher as BatcherType); return waitBatcher; } diff --git a/ts/util/waitForOnline.ts b/ts/util/waitForOnline.ts index 97127faaba..c84646ac50 100644 --- a/ts/util/waitForOnline.ts +++ b/ts/util/waitForOnline.ts @@ -4,7 +4,7 @@ import { clearTimeoutIfNecessary } from './clearTimeoutIfNecessary.js'; export type WaitForOnlineOptionsType = Readonly<{ - server?: Readonly<{ isOnline: () => boolean | undefined }>; + server: Readonly<{ isOnline: () => boolean | undefined }>; events?: { on: (event: 'online', fn: () => void) => void; off: (event: 'online', fn: () => void) => void; @@ -13,20 +13,11 @@ export type WaitForOnlineOptionsType = Readonly<{ }>; export function waitForOnline({ - server: maybeServer, + server, events = window.Whisper.events, timeout, -}: WaitForOnlineOptionsType = {}): Promise { +}: WaitForOnlineOptionsType): Promise { return new Promise((resolve, reject) => { - let server = maybeServer; - if (server === undefined) { - ({ server } = window.textsecure); - if (!server) { - reject(new Error('waitForOnline: no textsecure server')); - return; - } - } - if (server.isOnline()) { resolve(); return; diff --git a/ts/util/whatTypeOfConversation.ts b/ts/util/whatTypeOfConversation.ts index b71e3da75e..c74af38cde 100644 --- a/ts/util/whatTypeOfConversation.ts +++ b/ts/util/whatTypeOfConversation.ts @@ -30,8 +30,9 @@ export function isMe( conversationAttrs: Pick ): boolean { const { e164, serviceId } = conversationAttrs; - const ourNumber = window.textsecure.storage.user.getNumber(); - const ourAci = window.textsecure.storage.user.getAci(); + const us = window.ConversationController.getOurConversation(); + const ourNumber = us?.get('e164'); + const ourAci = us?.get('serviceId'); return Boolean( (e164 && e164 === ourNumber) || (serviceId && serviceId === ourAci) ); diff --git a/ts/util/wrapWithSyncMessageSend.ts b/ts/util/wrapWithSyncMessageSend.ts index 8cab9497e2..808bc36232 100644 --- a/ts/util/wrapWithSyncMessageSend.ts +++ b/ts/util/wrapWithSyncMessageSend.ts @@ -10,7 +10,10 @@ import { handleMessageSend } from './handleMessageSend.js'; import type { CallbackResultType } from '../textsecure/Types.d.ts'; import type { ConversationModel } from '../models/conversations.js'; import type { SendTypesType } from './handleMessageSend.js'; -import type MessageSender from '../textsecure/SendMessage.js'; +import { + type MessageSender, + messageSender, +} from '../textsecure/SendMessage.js'; import { areAllErrorsUnregistered } from '../jobs/helpers/areAllErrorsUnregistered.js'; const log = createLogger('wrapWithSyncMessageSend'); @@ -31,17 +34,16 @@ export async function wrapWithSyncMessageSend({ timestamp: number; }): Promise { const logId = `wrapWithSyncMessageSend(${parentLogId}, ${timestamp})`; - const sender = window.textsecure.messaging; - if (!sender) { - throw new Error(`${logId}: textsecure.messaging is not available!`); - } let response: CallbackResultType | undefined; let error: Error | undefined; let didSuccessfullySendOne = false; try { - response = await handleMessageSend(send(sender), { messageIds, sendType }); + response = await handleMessageSend(send(messageSender), { + messageIds, + sendType, + }); didSuccessfullySendOne = true; } catch (thrown) { if (thrown instanceof SendMessageProtoError) { @@ -77,7 +79,7 @@ export async function wrapWithSyncMessageSend({ syncMessage: true, }); await handleMessageSend( - sender.sendSyncMessage({ + messageSender.sendSyncMessage({ destinationE164: conversation.get('e164'), destinationServiceId: conversation.getServiceId(), encodedDataMessage: dataMessage, diff --git a/ts/window.d.ts b/ts/window.d.ts index 07ce426ade..699de8e796 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -10,8 +10,6 @@ import type PQueue from 'p-queue/dist.js'; import type { assert } from 'chai'; import type { MochaOptions } from 'mocha'; -import type { textsecure } from './textsecure/index.js'; -import type { Storage } from './textsecure/Storage.js'; import type { ChallengeHandler, IPCRequest as IPCChallengeRequest, @@ -186,7 +184,6 @@ declare global { preloadedImages: Array; setImmediate: typeof setImmediate; sendChallengeRequest: (request: IPCChallengeRequest) => void; - storage: Storage; systemTheme: SystemThemeType; Signal: SignalCoreType; @@ -205,7 +202,6 @@ declare global { i18n: LocalizerType; // Note: used in background.html, and not type-checked startApp: () => void; - textsecure: typeof textsecure; // IPC IPC: IPCType; diff --git a/ts/windows/main/phase1-ipc.ts b/ts/windows/main/phase1-ipc.ts index 627a309dee..6e570dc7e0 100644 --- a/ts/windows/main/phase1-ipc.ts +++ b/ts/windows/main/phase1-ipc.ts @@ -37,6 +37,7 @@ import { conversationQueueJobEnum, } from '../../jobs/conversationJobQueue.js'; import { isEnabled } from '../../RemoteConfig.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { groupBy, mapValues } = lodash; @@ -253,7 +254,7 @@ ipc.on('additional-log-data-request', async event => { ? ourConversation.get('capabilities') : undefined; - const remoteConfig = window.storage.get('remoteConfig') || {}; + const remoteConfig = itemStorage.get('remoteConfig') || {}; let statistics; try { @@ -289,8 +290,8 @@ ipc.on('additional-log-data-request', async event => { }; } - const ourAci = window.textsecure.storage.user.getAci(); - const ourPni = window.textsecure.storage.user.getPni(); + const ourAci = itemStorage.user.getAci(); + const ourPni = itemStorage.user.getPni(); event.sender.send('additional-log-data-response', { capabilities: ourCapabilities || {}, @@ -304,7 +305,7 @@ ipc.on('additional-log-data-request', async event => { ...networkStatistics, }, user: { - deviceId: window.textsecure.storage.user.getDeviceId(), + deviceId: itemStorage.user.getDeviceId(), uuid: ourAci, pni: ourPni, conversationId: ourConversation && ourConversation.id, diff --git a/ts/windows/main/phase2-dependencies.ts b/ts/windows/main/phase2-dependencies.ts index 069c5bfd32..13319d00b8 100644 --- a/ts/windows/main/phase2-dependencies.ts +++ b/ts/windows/main/phase2-dependencies.ts @@ -5,7 +5,6 @@ import * as moment from 'moment'; // @ts-expect-error -- no types import 'moment/min/locales.min.js'; -import { textsecure } from '../../textsecure/index.js'; import { initialize as initializeLogging } from '../../logging/set_up_renderer_logging.js'; import { setup } from '../../signal.js'; import { addSensitivePath } from '../../util/privacy.js'; @@ -13,13 +12,13 @@ import * as dns from '../../util/dns.js'; import { createLogger } from '../../logging/log.js'; import { SignalContext } from '../context.js'; import * as Attachments from './attachments.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const log = createLogger('phase2-dependencies'); initializeLogging(); window.nodeSetImmediate = setImmediate; -window.textsecure = textsecure; const { config } = window.SignalContext; @@ -57,7 +56,7 @@ dns.setFallback(SignalContext.config.dnsFallback); window.Signal = setup({ Attachments, - getRegionCode: () => window.storage.get('regionCode'), + getRegionCode: () => itemStorage.get('regionCode'), logger: log, userDataPath, }); diff --git a/ts/windows/main/preload_test.ts b/ts/windows/main/preload_test.ts index 90bb835c52..3dcc0a32a5 100644 --- a/ts/windows/main/preload_test.ts +++ b/ts/windows/main/preload_test.ts @@ -19,6 +19,7 @@ import { initializeMessageCounter } from '../../util/incrementMessageCounter.js' import { initializeRedux } from '../../state/initializeRedux.js'; import * as Stickers from '../../types/Stickers.js'; import { ThemeType } from '../../types/Util.js'; +import { itemStorage } from '../../textsecure/Storage.js'; chai.use(chaiAsPromised); @@ -139,6 +140,8 @@ window.testUtilities = { }, theme: ThemeType.dark, }); + + await itemStorage.fetch(); }, prepareTests() { diff --git a/ts/windows/main/start.ts b/ts/windows/main/start.ts index af72f332f3..e12025cbea 100644 --- a/ts/windows/main/start.ts +++ b/ts/windows/main/start.ts @@ -22,6 +22,7 @@ import type { CdsLookupOptionsType, GetIceServersResultType, } from '../../textsecure/WebAPI.js'; +import { cdsLookup, getSocketStatus } from '../../textsecure/WebAPI.js'; import type { FeatureFlagType } from '../../window.d.ts'; import type { StorageAccessType } from '../../types/Storage.d.ts'; import { initMessageCleanup } from '../../services/messageStateCleanup.js'; @@ -29,6 +30,7 @@ import { calling } from '../../services/calling.js'; import { Environment, getEnvironment } from '../../environment.js'; import { isProduction } from '../../util/version.js'; import { benchmarkConversationOpen } from '../../CI/benchmarkConversationOpen.js'; +import { itemStorage } from '../../textsecure/Storage.js'; const { has } = lodash; @@ -60,8 +62,7 @@ if ( window.SignalContext.config.devTools ) { const SignalDebug = { - cdsLookup: (options: CdsLookupOptionsType) => - window.textsecure.server?.cdsLookup(options), + cdsLookup: (options: CdsLookupOptionsType) => cdsLookup(options), getSelectedConversation: () => { const conversationId = window.reduxStore.getState().conversations.selectedConversationId; @@ -86,12 +87,12 @@ if ( getReduxState: () => window.reduxStore.getState(), getSfuUrl: () => calling._sfuUrl, getIceServerOverride: () => calling._iceServerOverride, - getSocketStatus: () => window.textsecure.server?.getSocketStatus(), - getStorageItem: (name: keyof StorageAccessType) => window.storage.get(name), + getSocketStatus: () => getSocketStatus(), + getStorageItem: (name: keyof StorageAccessType) => itemStorage.get(name), putStorageItem: ( name: K, value: StorageAccessType[K] - ) => window.storage.put(name, value), + ) => itemStorage.put(name, value), setFlag: (name: keyof FeatureFlagType, value: boolean) => { if (!has(window.Flags, name)) { return;