diff --git a/ts/ConversationController.preload.ts b/ts/ConversationController.preload.ts index 0a708450d1..d33b6140d7 100644 --- a/ts/ConversationController.preload.ts +++ b/ts/ConversationController.preload.ts @@ -51,6 +51,7 @@ import { validateConversation } from './util/validateConversation.dom.js'; import { ConversationModel } from './models/conversations.preload.js'; import { INITIAL_EXPIRE_TIMER_VERSION } from './util/expirationTimer.std.js'; import { missingCaseError } from './util/missingCaseError.std.js'; +import { removeConversation } from './util/Conversation.preload.js'; import { signalProtocolStore } from './SignalProtocolStore.preload.js'; import type { @@ -159,7 +160,6 @@ const { getAllConversations, getMessagesBySentAt } = DataReader; const { migrateConversationMessages, - removeConversation, saveConversation, updateConversation, updateConversations, @@ -370,6 +370,7 @@ export class ConversationController { conversationsUpdated([conversation.format()]); } } + #removeConversation(conversation: ConversationModel): void { this.#_conversations = without(this.#_conversations, conversation); this.#removeFromLookup(conversation); diff --git a/ts/LibSignalStores.preload.ts b/ts/LibSignalStores.node.ts similarity index 70% rename from ts/LibSignalStores.preload.ts rename to ts/LibSignalStores.node.ts index 089bb67756..9122f1e15d 100644 --- a/ts/LibSignalStores.preload.ts +++ b/ts/LibSignalStores.node.ts @@ -32,7 +32,7 @@ import { Address } from './types/Address.std.js'; import { QualifiedAddress } from './types/QualifiedAddress.std.js'; import type { ServiceIdString } from './types/ServiceId.std.js'; import { normalizeServiceId } from './types/ServiceId.std.js'; -import { signalProtocolStore } from './SignalProtocolStore.preload.js'; +import type { SignalProtocolStore } from './SignalProtocolStore.preload.js'; import type { Zone } from './util/Zone.std.js'; @@ -52,17 +52,20 @@ function toQualifiedAddress( } export type SessionsOptions = Readonly<{ + signalProtocolStore: SignalProtocolStore; ourServiceId: ServiceIdString; zone?: Zone; }>; export class Sessions extends SessionStore { + readonly #signalProtocolStore: SignalProtocolStore; readonly #ourServiceId: ServiceIdString; readonly #zone: Zone | undefined; - constructor({ ourServiceId, zone }: SessionsOptions) { + constructor({ signalProtocolStore, ourServiceId, zone }: SessionsOptions) { super(); + this.#signalProtocolStore = signalProtocolStore; this.#ourServiceId = ourServiceId; this.#zone = zone; } @@ -71,7 +74,7 @@ export class Sessions extends SessionStore { address: ProtocolAddress, record: SessionRecord ): Promise { - await signalProtocolStore.storeSession( + await this.#signalProtocolStore.storeSession( toQualifiedAddress(this.#ourServiceId, address), record, { zone: this.#zone } @@ -80,7 +83,7 @@ export class Sessions extends SessionStore { async getSession(name: ProtocolAddress): Promise { const encodedAddress = toQualifiedAddress(this.#ourServiceId, name); - const record = await signalProtocolStore.loadSession(encodedAddress, { + const record = await this.#signalProtocolStore.loadSession(encodedAddress, { zone: this.#zone, }); @@ -93,30 +96,39 @@ export class Sessions extends SessionStore { const encodedAddresses = addresses.map(addr => toQualifiedAddress(this.#ourServiceId, addr) ); - return signalProtocolStore.loadSessions(encodedAddresses, { + return this.#signalProtocolStore.loadSessions(encodedAddresses, { zone: this.#zone, }); } } export type IdentityKeysOptions = Readonly<{ + signalProtocolStore: SignalProtocolStore; ourServiceId: ServiceIdString; zone?: Zone; }>; export class IdentityKeys extends IdentityKeyStore { + readonly #signalProtocolStore: SignalProtocolStore; readonly #ourServiceId: ServiceIdString; readonly #zone: Zone | undefined; - constructor({ ourServiceId, zone }: IdentityKeysOptions) { + constructor({ + signalProtocolStore, + ourServiceId, + zone, + }: IdentityKeysOptions) { super(); + this.#signalProtocolStore = signalProtocolStore; this.#ourServiceId = ourServiceId; this.#zone = zone; } async getIdentityKey(): Promise { - const keyPair = signalProtocolStore.getIdentityKeyPair(this.#ourServiceId); + const keyPair = this.#signalProtocolStore.getIdentityKeyPair( + this.#ourServiceId + ); if (!keyPair) { throw new Error('IdentityKeyStore/getIdentityKey: No identity key!'); } @@ -124,7 +136,7 @@ export class IdentityKeys extends IdentityKeyStore { } async getLocalRegistrationId(): Promise { - const id = await signalProtocolStore.getLocalRegistrationId( + const id = await this.#signalProtocolStore.getLocalRegistrationId( this.#ourServiceId ); if (!isNumber(id)) { @@ -137,7 +149,7 @@ export class IdentityKeys extends IdentityKeyStore { async getIdentity(address: ProtocolAddress): Promise { const encodedAddress = encodeAddress(address); - const key = await signalProtocolStore.loadIdentityKey( + const key = await this.#signalProtocolStore.loadIdentityKey( encodedAddress.serviceId ); @@ -157,9 +169,14 @@ export class IdentityKeys extends IdentityKeyStore { // Pass `zone` to let `saveIdentity` archive sibling sessions when identity // key changes. - return signalProtocolStore.saveIdentity(encodedAddress, publicKey, false, { - zone: this.#zone, - }); + return this.#signalProtocolStore.saveIdentity( + encodedAddress, + publicKey, + false, + { + zone: this.#zone, + } + ); } async isTrustedIdentity( @@ -170,7 +187,7 @@ export class IdentityKeys extends IdentityKeyStore { const encodedAddress = encodeAddress(name); const publicKey = key.serialize(); - return signalProtocolStore.isTrustedIdentity( + return this.#signalProtocolStore.isTrustedIdentity( encodedAddress, publicKey, direction @@ -179,16 +196,19 @@ export class IdentityKeys extends IdentityKeyStore { } export type PreKeysOptions = Readonly<{ + signalProtocolStore: SignalProtocolStore; ourServiceId: ServiceIdString; zone?: Zone; }>; export class PreKeys extends PreKeyStore { + readonly #signalProtocolStore: SignalProtocolStore; readonly #ourServiceId: ServiceIdString; readonly #zone: Zone | undefined; - constructor({ ourServiceId, zone }: PreKeysOptions) { + constructor({ signalProtocolStore, ourServiceId, zone }: PreKeysOptions) { super(); + this.#signalProtocolStore = signalProtocolStore; this.#ourServiceId = ourServiceId; this.#zone = zone; } @@ -198,7 +218,10 @@ export class PreKeys extends PreKeyStore { } async getPreKey(id: number): Promise { - const preKey = await signalProtocolStore.loadPreKey(this.#ourServiceId, id); + const preKey = await this.#signalProtocolStore.loadPreKey( + this.#ourServiceId, + id + ); if (preKey === undefined) { throw new Error(`getPreKey: PreKey ${id} not found`); @@ -208,18 +231,20 @@ export class PreKeys extends PreKeyStore { } async removePreKey(id: number): Promise { - await signalProtocolStore.removePreKeys(this.#ourServiceId, [id], { + await this.#signalProtocolStore.removePreKeys(this.#ourServiceId, [id], { zone: this.#zone, }); } } export class KyberPreKeys extends KyberPreKeyStore { + readonly #signalProtocolStore: SignalProtocolStore; readonly #ourServiceId: ServiceIdString; readonly #zone: Zone | undefined; - constructor({ ourServiceId, zone }: PreKeysOptions) { + constructor({ signalProtocolStore, ourServiceId, zone }: PreKeysOptions) { super(); + this.#signalProtocolStore = signalProtocolStore; this.#ourServiceId = ourServiceId; this.#zone = zone; } @@ -229,7 +254,7 @@ export class KyberPreKeys extends KyberPreKeyStore { } async getKyberPreKey(id: number): Promise { - const kyberPreKey = await signalProtocolStore.loadKyberPreKey( + const kyberPreKey = await this.#signalProtocolStore.loadKyberPreKey( this.#ourServiceId, id ); @@ -246,7 +271,7 @@ export class KyberPreKeys extends KyberPreKeyStore { signedPreKeyId: number, baseKey: PublicKey ): Promise { - await signalProtocolStore.maybeRemoveKyberPreKey( + await this.#signalProtocolStore.maybeRemoveKyberPreKey( this.#ourServiceId, { keyId, signedPreKeyId, baseKey }, { zone: this.#zone } @@ -255,16 +280,19 @@ export class KyberPreKeys extends KyberPreKeyStore { } export type SenderKeysOptions = Readonly<{ - readonly ourServiceId: ServiceIdString; - readonly zone: Zone | undefined; + signalProtocolStore: SignalProtocolStore; + ourServiceId: ServiceIdString; + zone: Zone | undefined; }>; export class SenderKeys extends SenderKeyStore { + readonly #signalProtocolStore: SignalProtocolStore; readonly #ourServiceId: ServiceIdString; readonly zone: Zone | undefined; - constructor({ ourServiceId, zone }: SenderKeysOptions) { + constructor({ signalProtocolStore, ourServiceId, zone }: SenderKeysOptions) { super(); + this.#signalProtocolStore = signalProtocolStore; this.#ourServiceId = ourServiceId; this.zone = zone; } @@ -276,7 +304,7 @@ export class SenderKeys extends SenderKeyStore { ): Promise { const encodedAddress = toQualifiedAddress(this.#ourServiceId, sender); - await signalProtocolStore.saveSenderKey( + await this.#signalProtocolStore.saveSenderKey( encodedAddress, distributionId, record, @@ -290,7 +318,7 @@ export class SenderKeys extends SenderKeyStore { ): Promise { const encodedAddress = toQualifiedAddress(this.#ourServiceId, sender); - const senderKey = await signalProtocolStore.getSenderKey( + const senderKey = await this.#signalProtocolStore.getSenderKey( encodedAddress, distributionId, { zone: this.zone } @@ -301,15 +329,18 @@ export class SenderKeys extends SenderKeyStore { } export type SignedPreKeysOptions = Readonly<{ + signalProtocolStore: SignalProtocolStore; ourServiceId: ServiceIdString; }>; // No need for zone awareness, since no mutation happens in this store export class SignedPreKeys extends SignedPreKeyStore { + readonly #signalProtocolStore: SignalProtocolStore; readonly #ourServiceId: ServiceIdString; - constructor({ ourServiceId }: SignedPreKeysOptions) { + constructor({ signalProtocolStore, ourServiceId }: SignedPreKeysOptions) { super(); + this.#signalProtocolStore = signalProtocolStore; this.#ourServiceId = ourServiceId; } @@ -318,7 +349,7 @@ export class SignedPreKeys extends SignedPreKeyStore { } async getSignedPreKey(id: number): Promise { - const signedPreKey = await signalProtocolStore.loadSignedPreKey( + const signedPreKey = await this.#signalProtocolStore.loadSignedPreKey( this.#ourServiceId, id ); @@ -332,24 +363,30 @@ export class SignedPreKeys extends SignedPreKeyStore { } export class KeyTransparencyStore implements KeyTransparencyStoreInterface { + readonly #signalProtocolStore: SignalProtocolStore; + + constructor(signalProtocolStore: SignalProtocolStore) { + this.#signalProtocolStore = signalProtocolStore; + } + async getLastDistinguishedTreeHead(): Promise | null> { - return signalProtocolStore.getLastDistinguishedTreeHead(); + return this.#signalProtocolStore.getLastDistinguishedTreeHead(); } async setLastDistinguishedTreeHead( bytes: Readonly> | null ): Promise { - return signalProtocolStore.setLastDistinguishedTreeHead(bytes); + return this.#signalProtocolStore.setLastDistinguishedTreeHead(bytes); } async getAccountData(aci: Aci): Promise | null> { - return signalProtocolStore.getKTAccountData(aci); + return this.#signalProtocolStore.getKTAccountData(aci); } async setAccountData( aci: Aci, bytes: Readonly> ): Promise { - return signalProtocolStore.setKTAccountData(aci, bytes); + return this.#signalProtocolStore.setKTAccountData(aci, bytes); } } diff --git a/ts/groups/joinViaLink.preload.ts b/ts/groups/joinViaLink.preload.ts index e43d0139fb..1df0177d9c 100644 --- a/ts/groups/joinViaLink.preload.ts +++ b/ts/groups/joinViaLink.preload.ts @@ -36,6 +36,7 @@ import { dropNull } from '../util/dropNull.std.js'; import { getLocalAttachmentUrl } from '../util/getLocalAttachmentUrl.std.js'; import { type Loadable, LoadingState } from '../util/loadable.std.js'; import { missingCaseError } from '../util/missingCaseError.std.js'; +import { removeConversation } from '../util/Conversation.preload.js'; import { itemStorage } from '../textsecure/Storage.preload.js'; const log = createLogger('joinViaLink'); @@ -371,7 +372,7 @@ export async function joinViaLink(value: string): Promise { window.ConversationController.dangerouslyRemoveById( tempConversation.id ); - await DataWriter.removeConversation(tempConversation.id); + await removeConversation(tempConversation.id); } throw error; diff --git a/ts/jobs/AttachmentDownloadManager.preload.ts b/ts/jobs/AttachmentDownloadManager.preload.ts index f654e582fa..01e8af70e1 100644 --- a/ts/jobs/AttachmentDownloadManager.preload.ts +++ b/ts/jobs/AttachmentDownloadManager.preload.ts @@ -86,7 +86,7 @@ import { JobCancelReason } from './types.std.js'; import { isAbortError } from '../util/isAbortError.std.js'; import { itemStorage } from '../textsecure/Storage.preload.js'; import { calculateExpirationTimestamp } from '../util/expirationTimer.std.js'; -import { cleanupAttachmentFiles } from '../types/Message2.preload.js'; +import { cleanupAttachmentFiles } from '../util/cleanup.preload.js'; import { getExistingAttachmentDataForReuse } from '../util/attachments/deduplicateAttachment.preload.js'; const { noop, omit, throttle } = lodash; diff --git a/ts/services/calling.preload.ts b/ts/services/calling.preload.ts index d77c4012f1..aa259edca5 100644 --- a/ts/services/calling.preload.ts +++ b/ts/services/calling.preload.ts @@ -67,7 +67,7 @@ import type { GroupCallPeekInfoType, } from '../state/ducks/calling.preload.js'; import type { ConversationType } from '../state/ducks/conversations.preload.js'; -import { getConversationCallMode } from '../state/ducks/conversations.preload.js'; +import { getConversationCallMode } from '../util/getConversationCallMode.std.js'; import { isMe } from '../util/whatTypeOfConversation.dom.js'; import { getAbsoluteTempPath } from '../util/migrations.preload.js'; import { isKnownProtoEnumMember } from '../util/isKnownProtoEnumMember.std.js'; diff --git a/ts/sql/Client.preload.ts b/ts/sql/Client.preload.ts index 91cafbdcb6..0340abfa8d 100644 --- a/ts/sql/Client.preload.ts +++ b/ts/sql/Client.preload.ts @@ -13,11 +13,9 @@ import * as Bytes from '../Bytes.std.js'; import { createLogger } from '../logging/log.std.js'; import * as Errors from '../types/errors.std.js'; -import { deleteExternalFiles } from '../types/Conversation.std.js'; import { createBatcher } from '../util/batcher.std.js'; import { assertDev, softAssert } from '../util/assert.std.js'; import { mapObjectWithSpec } from '../util/mapObjectWithSpec.std.js'; -import { maybeDeleteAttachmentFile } from '../util/migrations.preload.js'; import { cleanDataForIpc } from './cleanDataForIpc.std.js'; import { runTaskWithTimeout } from '../textsecure/TaskWithTimeout.std.js'; import { isValidUuid, isValidUuidV7 } from '../util/isValidUuid.std.js'; @@ -131,7 +129,6 @@ const clientOnlyWritable: ClientOnlyWritableInterface = { createOrUpdateItem, updateConversation, - removeConversation, removeMessageById, removeMessagesById, @@ -553,19 +550,6 @@ async function updateConversations( await writableChannel.updateConversations(cleaned); } -async function removeConversation(id: string): Promise { - const existing = await readableChannel.getConversationById(id); - - // Note: It's important to have a fully database-hydrated model to delete here because - // it needs to delete all associated on-disk files along with the database delete. - if (existing) { - await writableChannel.removeConversation(id); - await deleteExternalFiles(existing, { - maybeDeleteAttachmentFile, - }); - } -} - function handleSearchMessageJSON( messages: Array ): Array { diff --git a/ts/sql/Interface.std.ts b/ts/sql/Interface.std.ts index 77fceed0eb..ce074468df 100644 --- a/ts/sql/Interface.std.ts +++ b/ts/sql/Interface.std.ts @@ -1190,7 +1190,8 @@ type WritableInterface = { saveConversations: (array: Array) => void; // updateConversation is a normal data method on Server, a sync batch-add on Client updateConversations: (array: Array) => void; - // removeConversation handles either one id or an array on Server, and one id on Client + /** @internal use ts/util/Conversation.preload.ts */ + _removeConversation: (id: string) => void; _removeAllConversations: () => void; updateAllConversationColors: ( conversationColor?: ConversationColorType, @@ -1583,7 +1584,6 @@ export type ServerWritableDirectInterface = WritableInterface & { // Differing signature on client/server updateConversation: (data: ConversationType) => void; - removeConversation: (id: Array | string) => void; saveMessage: ( data: ReadonlyDeep, @@ -1689,7 +1689,6 @@ export type ClientOnlyReadableInterface = ClientInterfaceWrap<{ export type ClientOnlyWritableInterface = ClientInterfaceWrap<{ // Differing signature on client/server updateConversation: (data: ConversationType) => void; - removeConversation: (id: string) => void; flushUpdateConversationBatcher: () => void; saveMessage: ( diff --git a/ts/sql/Server.node.ts b/ts/sql/Server.node.ts index 04f071c86e..a0affb528a 100644 --- a/ts/sql/Server.node.ts +++ b/ts/sql/Server.node.ts @@ -637,7 +637,7 @@ export const DataWriter: ServerWritableInterface = { saveConversations, updateConversation, updateConversations, - removeConversation, + _removeConversation, _removeAllConversations, updateAllConversationColors, removeAllProfileKeyCredentials, @@ -2046,37 +2046,10 @@ function updateConversations( })(); } -function removeConversations( - db: WritableDB, - ids: ReadonlyArray, - persistent: boolean -): void { - // Our node interface doesn't seem to allow you to replace one single ? with an array - db.prepare( - ` - DELETE FROM conversations - WHERE id IN ( ${ids.map(() => '?').join(', ')} ); - `, - { persistent } - ).run(ids); -} - -function removeConversation(db: WritableDB, id: Array | string): void { - if (!Array.isArray(id)) { - db.prepare('DELETE FROM conversations WHERE id = $id;').run({ - id, - }); - - return; - } - - if (!id.length) { - throw new Error('removeConversation: No ids to delete!'); - } - - batchMultiVarQuery(db, id, (ids, persistent) => - removeConversations(db, ids, persistent) - ); +function _removeConversation(db: WritableDB, id: string): void { + db.prepare('DELETE FROM conversations WHERE id = $id;').run({ + id, + }); } function _removeAllConversations(db: WritableDB): void { diff --git a/ts/state/ducks/calling.preload.ts b/ts/state/ducks/calling.preload.ts index de2c266ff8..c6a2a22275 100644 --- a/ts/state/ducks/calling.preload.ts +++ b/ts/state/ducks/calling.preload.ts @@ -69,12 +69,10 @@ import type { ConversationsUpdatedActionType, ConversationRemovedActionType, } from './conversations.preload.js'; -import { - getConversationCallMode, - updateLastMessage, -} from './conversations.preload.js'; +import { updateLastMessage } from './conversations.preload.js'; import { createLogger } from '../../logging/log.std.js'; import { strictAssert } from '../../util/assert.std.js'; +import { getConversationCallMode } from '../../util/getConversationCallMode.std.js'; import { waitForOnline } from '../../util/waitForOnline.dom.js'; import * as mapUtil from '../../util/mapUtil.std.js'; import { isCallSafe } from '../../util/isCallSafe.dom.js'; diff --git a/ts/state/ducks/conversations.preload.ts b/ts/state/ducks/conversations.preload.ts index ebfa5dc72a..7e1fcb0930 100644 --- a/ts/state/ducks/conversations.preload.ts +++ b/ts/state/ducks/conversations.preload.ts @@ -657,31 +657,6 @@ export type ConversationsStateType = ReadonlyDeep<{ hasProfileUpdateError?: boolean; }>; -// Helpers - -export const getConversationCallMode = ( - conversation: ConversationType -): CallMode | null => { - if ( - conversation.left || - conversation.isBlocked || - conversation.isMe || - !conversation.acceptedMessageRequest - ) { - return null; - } - - if (conversation.type === 'direct') { - return CallMode.Direct; - } - - if (conversation.type === 'group' && conversation.groupVersion === 2) { - return CallMode.Group; - } - - return null; -}; - // Actions const CANCEL_CONVERSATION_PENDING_VERIFICATION = diff --git a/ts/state/selectors/message.preload.ts b/ts/state/selectors/message.preload.ts index 6a3f89c5b7..2ede806ddf 100644 --- a/ts/state/selectors/message.preload.ts +++ b/ts/state/selectors/message.preload.ts @@ -97,7 +97,7 @@ import { AttachmentDisposition, } from '../../util/getLocalAttachmentUrl.std.js'; import { isVoiceMessagePlayed } from '../../util/isVoiceMessagePlayed.std.js'; -import { isPermanentlyUndownloadable } from '../../jobs/AttachmentDownloadManager.preload.js'; +import { isPermanentlyUndownloadable } from '../../jobs/helpers/attachmentBackfill.preload.js'; import { getAccountSelector } from './accounts.std.js'; import { diff --git a/ts/state/smart/ConversationHeader.preload.tsx b/ts/state/smart/ConversationHeader.preload.tsx index 4d2a7f194d..f4f3e5a3e7 100644 --- a/ts/state/smart/ConversationHeader.preload.tsx +++ b/ts/state/smart/ConversationHeader.preload.tsx @@ -20,13 +20,11 @@ import { isConversationSMSOnly } from '../../util/isConversationSMSOnly.std.js'; import { isGroupOrAdhocCallState } from '../../util/isGroupOrAdhocCall.std.js'; import { isSignalConversation } from '../../util/isSignalConversation.dom.js'; import { missingCaseError } from '../../util/missingCaseError.std.js'; +import { getConversationCallMode } from '../../util/getConversationCallMode.std.js'; import { useCallingActions } from '../ducks/calling.preload.js'; import { isAnybodyElseInGroupCall } from '../ducks/callingHelpers.std.js'; import type { ConversationType } from '../ducks/conversations.preload.js'; -import { - getConversationCallMode, - useConversationsActions, -} from '../ducks/conversations.preload.js'; +import { useConversationsActions } from '../ducks/conversations.preload.js'; import { useSearchActions } from '../ducks/search.preload.js'; import { useStoriesActions } from '../ducks/stories.preload.js'; import { getPreferredBadgeSelector } from '../selectors/badges.preload.js'; diff --git a/ts/test-electron/deleteMessageAttachments_test.preload.ts b/ts/test-electron/deleteMessageAttachments_test.preload.ts index 24f8134261..af332bfe22 100644 --- a/ts/test-electron/deleteMessageAttachments_test.preload.ts +++ b/ts/test-electron/deleteMessageAttachments_test.preload.ts @@ -26,14 +26,14 @@ import { strictAssert } from '../util/assert.std.js'; import { cleanupAllMessageAttachmentFiles, cleanupAttachmentFiles, -} from '../types/Message2.preload.js'; + cleanupMessages, +} from '../util/cleanup.preload.js'; import { DataReader, DataWriter } from '../sql/Client.preload.js'; import { generateAci } from '../types/ServiceId.std.js'; import { testAttachmentLocalKey, testPlaintextHash, } from '../test-helpers/attachments.node.js'; -import { cleanupMessages } from '../util/cleanup.preload.js'; const { emptyDir, ensureFile } = fsExtra; diff --git a/ts/test-electron/state/ducks/conversations_test.preload.ts b/ts/test-electron/state/ducks/conversations_test.preload.ts index a8b03af357..debbf1d8f4 100644 --- a/ts/test-electron/state/ducks/conversations_test.preload.ts +++ b/ts/test-electron/state/ducks/conversations_test.preload.ts @@ -30,7 +30,6 @@ import { actions, cancelConversationVerification, clearCanceledConversationVerification, - getConversationCallMode, getEmptyState, reducer, updateConversationLookups, @@ -69,6 +68,7 @@ import { import { MY_STORY_ID } from '../../../types/Stories.std.js'; import type { ReadonlyMessageAttributesType } from '../../../model-types.d.ts'; import { strictAssert } from '../../../util/assert.std.js'; +import { getConversationCallMode } from '../../../util/getConversationCallMode.std.js'; import { itemStorage } from '../../../textsecure/Storage.preload.js'; const { times } = lodash; diff --git a/ts/test-electron/textsecure/KeyChangeListener_test.preload.ts b/ts/test-electron/textsecure/KeyChangeListener_test.preload.ts index db160b2f4e..d65925ea6a 100644 --- a/ts/test-electron/textsecure/KeyChangeListener_test.preload.ts +++ b/ts/test-electron/textsecure/KeyChangeListener_test.preload.ts @@ -14,6 +14,7 @@ import * as KeyChangeListener from '../../textsecure/KeyChangeListener.dom.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; import * as Bytes from '../../Bytes.std.js'; import { cleanupMessages } from '../../util/cleanup.preload.js'; +import { removeConversation } from '../../util/Conversation.preload.js'; describe('KeyChangeListener', () => { let oldNumberId: string | undefined; @@ -71,7 +72,7 @@ describe('KeyChangeListener', () => { logId: ourServiceIdWithKeyChange, cleanupMessages, }); - await DataWriter.removeConversation(convo.id); + await removeConversation(convo.id); await store.removeIdentityKey(ourServiceIdWithKeyChange); }); @@ -113,7 +114,7 @@ describe('KeyChangeListener', () => { logId: ourServiceIdWithKeyChange, cleanupMessages, }); - await DataWriter.removeConversation(groupConvo.id); + await removeConversation(groupConvo.id); }); it('generates a key change notice in the group conversation with this contact', async () => { diff --git a/ts/textsecure/MessageReceiver.preload.ts b/ts/textsecure/MessageReceiver.preload.ts index 853d8dbcf2..2f681b6b7a 100644 --- a/ts/textsecure/MessageReceiver.preload.ts +++ b/ts/textsecure/MessageReceiver.preload.ts @@ -38,7 +38,7 @@ import { SenderKeys, Sessions, SignedPreKeys, -} from '../LibSignalStores.preload.js'; +} from '../LibSignalStores.node.js'; import { createName } from '../util/attachmentPath.node.js'; import { assertDev, strictAssert } from '../util/assert.std.js'; import type { BatcherType } from '../util/batcher.std.js'; @@ -1022,6 +1022,7 @@ export default class MessageReceiver let stores = storesMap.get(destinationServiceId); if (!stores) { const sharedParams = { + signalProtocolStore, ourServiceId: destinationServiceId, zone, }; diff --git a/ts/textsecure/OutgoingMessage.preload.ts b/ts/textsecure/OutgoingMessage.preload.ts index 19a5156fe8..0ec81c1144 100644 --- a/ts/textsecure/OutgoingMessage.preload.ts +++ b/ts/textsecure/OutgoingMessage.preload.ts @@ -45,7 +45,7 @@ import * as Errors from '../types/errors.std.js'; import { HTTPError } from '../types/HTTPError.std.js'; import { QualifiedAddress } from '../types/QualifiedAddress.std.js'; import type { ServiceIdString } from '../types/ServiceId.std.js'; -import { Sessions, IdentityKeys } from '../LibSignalStores.preload.js'; +import { Sessions, IdentityKeys } from '../LibSignalStores.node.js'; import { getKeysForServiceId } from './getKeysForServiceId.preload.js'; import { SignalService as Proto } from '../protobuf/index.std.js'; import { createLogger } from '../logging/log.std.js'; @@ -438,8 +438,14 @@ export default class OutgoingMessage { ); } - const sessionStore = new Sessions({ ourServiceId: ourAci }); - const identityKeyStore = new IdentityKeys({ ourServiceId: ourAci }); + const sessionStore = new Sessions({ + signalProtocolStore, + ourServiceId: ourAci, + }); + const identityKeyStore = new IdentityKeys({ + signalProtocolStore, + ourServiceId: ourAci, + }); return Promise.all( deviceIds.map(async destinationDeviceId => { diff --git a/ts/textsecure/SendMessage.preload.ts b/ts/textsecure/SendMessage.preload.ts index b0c5d88335..998908181b 100644 --- a/ts/textsecure/SendMessage.preload.ts +++ b/ts/textsecure/SendMessage.preload.ts @@ -27,7 +27,7 @@ import { uuidToBytes } from '../util/uuidToBytes.std.js'; import { Address } from '../types/Address.std.js'; import { QualifiedAddress } from '../types/QualifiedAddress.std.js'; import type { StoryMessageRecipientsType } from '../types/Stories.std.js'; -import { SenderKeys } from '../LibSignalStores.preload.js'; +import { SenderKeys } from '../LibSignalStores.node.js'; import type { TextAttachmentType, UploadedAttachmentType, @@ -2722,6 +2722,7 @@ export class MessageSender { const senderKeyDistributionMessage = await signalProtocolStore.enqueueSenderKeyJob(address, async () => { const senderKeyStore = new SenderKeys({ + signalProtocolStore, ourServiceId: ourAci, zone: GLOBAL_ZONE, }); diff --git a/ts/textsecure/WebAPI.preload.ts b/ts/textsecure/WebAPI.preload.ts index 47ab9803f3..6a2eb8e89b 100644 --- a/ts/textsecure/WebAPI.preload.ts +++ b/ts/textsecure/WebAPI.preload.ts @@ -129,7 +129,8 @@ import { type RemoteMegaphoneId, } from '../types/Megaphone.std.js'; import { bindRemoteConfigToLibsignalNet } from '../LibsignalNetRemoteConfig.preload.js'; -import { KeyTransparencyStore } from '../LibSignalStores.preload.js'; +import { KeyTransparencyStore } from '../LibSignalStores.node.js'; +import { signalProtocolStore } from '../SignalProtocolStore.preload.js'; const { escapeRegExp, isNumber, throttle } = lodash; @@ -2501,7 +2502,7 @@ export async function keyTransparencySearch( throw new Error('Aborted'); } const kt = chat.keyTransparencyClient(); - const store = new KeyTransparencyStore(); + const store = new KeyTransparencyStore(signalProtocolStore); return kt.search(request, store, { abortSignal }); }); } @@ -2517,7 +2518,7 @@ export async function keyTransparencyMonitor( throw new Error('Aborted'); } const kt = chat.keyTransparencyClient(); - const store = new KeyTransparencyStore(); + const store = new KeyTransparencyStore(signalProtocolStore); return kt.monitor( { ...request, diff --git a/ts/textsecure/getKeysForServiceId.preload.ts b/ts/textsecure/getKeysForServiceId.preload.ts index 8eec5fcb83..353fd4b895 100644 --- a/ts/textsecure/getKeysForServiceId.preload.ts +++ b/ts/textsecure/getKeysForServiceId.preload.ts @@ -15,7 +15,7 @@ import { OutgoingIdentityKeyError, UnregisteredUserError, } from './Errors.std.js'; -import { Sessions, IdentityKeys } from '../LibSignalStores.preload.js'; +import { Sessions, IdentityKeys } from '../LibSignalStores.node.js'; import { Address } from '../types/Address.std.js'; import { QualifiedAddress } from '../types/QualifiedAddress.std.js'; import type { ServiceIdString } from '../types/ServiceId.std.js'; @@ -133,8 +133,14 @@ async function handleServerKeys( devicesToUpdate: Array | null ): Promise { const ourAci = itemStorage.user.getCheckedAci(); - const sessionStore = new Sessions({ ourServiceId: ourAci }); - const identityKeyStore = new IdentityKeys({ ourServiceId: ourAci }); + const sessionStore = new Sessions({ + signalProtocolStore, + ourServiceId: ourAci, + }); + const identityKeyStore = new IdentityKeys({ + signalProtocolStore, + ourServiceId: ourAci, + }); await Promise.all( response.devices.map(async device => { diff --git a/ts/types/Conversation.std.ts b/ts/types/Conversation.std.ts deleted file mode 100644 index 27b1510890..0000000000 --- a/ts/types/Conversation.std.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2018 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { ConversationAttributesType } from '../model-types.d.ts'; - -export async function deleteExternalFiles( - conversation: ConversationAttributesType, - { - maybeDeleteAttachmentFile, - }: { - maybeDeleteAttachmentFile: ( - path: string - ) => Promise<{ wasDeleted: boolean }>; - } -): Promise { - if (!conversation) { - return; - } - - const { avatar, profileAvatar } = conversation; - - if (avatar && avatar.path) { - await maybeDeleteAttachmentFile(avatar.path); - } - - if (profileAvatar && profileAvatar.path) { - await maybeDeleteAttachmentFile(profileAvatar.path); - } -} diff --git a/ts/types/Message2.preload.ts b/ts/types/Message2.preload.ts index fc6b07abab..829dce5967 100644 --- a/ts/types/Message2.preload.ts +++ b/ts/types/Message2.preload.ts @@ -56,14 +56,6 @@ import { deepClone } from '../util/deepClone.std.js'; import * as Bytes from '../Bytes.std.js'; import { isBodyTooLong } from '../util/longAttachment.std.js'; import type { MessageAttachmentType } from './AttachmentDownload.std.js'; -import { - getFilePathsReferencedByAttachment, - getFilePathsReferencedByMessage, -} from '../util/messageFilePaths.std.js'; -import { - deleteDownloadFile, - maybeDeleteAttachmentFile, -} from '../util/migrations.preload.js'; import type { getExistingAttachmentDataForReuse } from '../util/attachments/deduplicateAttachment.preload.js'; import type { getPlaintextHashForInMemoryAttachment } from '../AttachmentCrypto.node.js'; import { strictAssert } from '../util/assert.std.js'; @@ -1050,31 +1042,6 @@ export const loadStickerData = ( }; }; -export const cleanupAllMessageAttachmentFiles = async ( - message: MessageAttributesType -): Promise => { - const { externalAttachments, externalDownloads } = - getFilePathsReferencedByMessage(message); - await Promise.all( - [...externalAttachments].map(attachmentPath => - maybeDeleteAttachmentFile(attachmentPath) - ) - ); - await Promise.all( - [...externalDownloads].map(downloadPath => deleteDownloadFile(downloadPath)) - ); -}; - -export async function cleanupAttachmentFiles( - attachment: AttachmentType -): Promise { - const result = getFilePathsReferencedByAttachment(attachment); - await Promise.all( - [...result.externalAttachments].map(maybeDeleteAttachmentFile) - ); - await Promise.all([...result.externalDownloads].map(deleteDownloadFile)); -} - export async function migrateBodyAttachmentToDisk( message: MessageAttributesType, { logger, writeNewAttachmentData }: ContextType diff --git a/ts/util/Conversation.preload.ts b/ts/util/Conversation.preload.ts new file mode 100644 index 0000000000..a96070bbfe --- /dev/null +++ b/ts/util/Conversation.preload.ts @@ -0,0 +1,35 @@ +// Copyright 2018 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { DataReader, DataWriter } from '../sql/Client.preload.js'; +import type { ConversationAttributesType } from '../model-types.d.ts'; +import { maybeDeleteAttachmentFile } from './migrations.preload.js'; + +async function deleteExternalFiles( + conversation: ConversationAttributesType +): Promise { + if (!conversation) { + return; + } + + const { avatar, profileAvatar } = conversation; + + if (avatar && avatar.path) { + await maybeDeleteAttachmentFile(avatar.path); + } + + if (profileAvatar && profileAvatar.path) { + await maybeDeleteAttachmentFile(profileAvatar.path); + } +} + +export async function removeConversation(id: string): Promise { + const existing = await DataReader.getConversationById(id); + + // Note: It's important to have a fully database-hydrated model to delete here because + // it needs to delete all associated on-disk files along with the database delete. + if (existing) { + await DataWriter._removeConversation(id); + await deleteExternalFiles(existing); + } +} diff --git a/ts/util/cleanup.preload.ts b/ts/util/cleanup.preload.ts index ea59379dfb..ac0e7d76df 100644 --- a/ts/util/cleanup.preload.ts +++ b/ts/util/cleanup.preload.ts @@ -26,12 +26,20 @@ import { getMessageIdForLogging } from './idForLogging.preload.js'; import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue.preload.js'; import { MINUTE } from './durations/index.std.js'; import { drop } from './drop.std.js'; +import { + getFilePathsReferencedByAttachment, + getFilePathsReferencedByMessage, +} from './messageFilePaths.std.js'; +import { + deleteDownloadFile, + maybeDeleteAttachmentFile, +} from './migrations.preload.js'; import { hydrateStoryContext } from './hydrateStoryContext.preload.js'; import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion.preload.js'; import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService.preload.js'; import { throttledUpdateBackupMediaDownloadProgress } from './updateBackupMediaDownloadProgress.preload.js'; import { messageAttrsToPreserveAfterErase } from '../types/Message.std.js'; -import { cleanupAllMessageAttachmentFiles } from '../types/Message2.preload.js'; +import type { AttachmentType } from '../types/Attachment.std.js'; const log = createLogger('cleanup'); @@ -281,3 +289,28 @@ export async function maybeDeleteCall( await DataWriter.markCallHistoryDeleted(callId); window.reduxActions.callHistory.removeCallHistory(callId); } + +export const cleanupAllMessageAttachmentFiles = async ( + message: MessageAttributesType +): Promise => { + const { externalAttachments, externalDownloads } = + getFilePathsReferencedByMessage(message); + await Promise.all( + [...externalAttachments].map(attachmentPath => + maybeDeleteAttachmentFile(attachmentPath) + ) + ); + await Promise.all( + [...externalDownloads].map(downloadPath => deleteDownloadFile(downloadPath)) + ); +}; + +export async function cleanupAttachmentFiles( + attachment: AttachmentType +): Promise { + const result = getFilePathsReferencedByAttachment(attachment); + await Promise.all( + [...result.externalAttachments].map(maybeDeleteAttachmentFile) + ); + await Promise.all([...result.externalDownloads].map(deleteDownloadFile)); +} diff --git a/ts/util/deleteForMe.preload.ts b/ts/util/deleteForMe.preload.ts index 4c0a2aa83d..34ce1bdfac 100644 --- a/ts/util/deleteForMe.preload.ts +++ b/ts/util/deleteForMe.preload.ts @@ -11,13 +11,16 @@ import type { ConversationModel } from '../models/conversations.preload.js'; import type { AddressableMessage } from '../textsecure/messageReceiverEvents.std.js'; import type { AttachmentType } from '../types/Attachment.std.js'; import { MessageModel } from '../models/messages.preload.js'; -import { cleanupMessages, postSaveUpdates } from './cleanup.preload.js'; +import { + cleanupMessages, + cleanupAttachmentFiles, + postSaveUpdates, +} from './cleanup.preload.js'; import { findMatchingMessage, getMessageQueryFromTarget, } from './syncIdentifiers.preload.js'; import { itemStorage } from '../textsecure/Storage.preload.js'; -import { cleanupAttachmentFiles } from '../types/Message2.preload.js'; const { last, sortBy } = lodash; diff --git a/ts/util/getConversationCallMode.std.ts b/ts/util/getConversationCallMode.std.ts new file mode 100644 index 0000000000..0ba96fa819 --- /dev/null +++ b/ts/util/getConversationCallMode.std.ts @@ -0,0 +1,28 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { ConversationType } from '../state/ducks/conversations.preload.js'; +import { CallMode } from '../types/CallDisposition.std.js'; + +export const getConversationCallMode = ( + conversation: ConversationType +): CallMode | null => { + if ( + conversation.left || + conversation.isBlocked || + conversation.isMe || + !conversation.acceptedMessageRequest + ) { + return null; + } + + if (conversation.type === 'direct') { + return CallMode.Direct; + } + + if (conversation.type === 'group' && conversation.groupVersion === 2) { + return CallMode.Group; + } + + return null; +}; diff --git a/ts/util/sendToGroup.preload.ts b/ts/util/sendToGroup.preload.ts index cdee0a5284..b8a137b692 100644 --- a/ts/util/sendToGroup.preload.ts +++ b/ts/util/sendToGroup.preload.ts @@ -53,11 +53,7 @@ import { UnknownRecipientError, UnregisteredUserError, } from '../textsecure/Errors.std.js'; -import { - IdentityKeys, - SenderKeys, - Sessions, -} from '../LibSignalStores.preload.js'; +import { IdentityKeys, SenderKeys, Sessions } from '../LibSignalStores.node.js'; import type { ConversationModel } from '../models/conversations.preload.js'; import type { DeviceType, CallbackResultType } from '../textsecure/Types.d.ts'; import { getKeysForServiceId } from '../textsecure/getKeysForServiceId.preload.js'; @@ -1297,6 +1293,7 @@ async function encryptForSenderKey({ ); const ourAddress = getOurAddress(); const senderKeyStore = new SenderKeys({ + signalProtocolStore, ourServiceId: ourAci, zone: GLOBAL_ZONE, }); @@ -1341,8 +1338,14 @@ async function encryptForSenderKey({ .map(device => { return ProtocolAddress.new(device.serviceId, device.id); }); - const identityKeyStore = new IdentityKeys({ ourServiceId: ourAci }); - const sessionStore = new Sessions({ ourServiceId: ourAci }); + const identityKeyStore = new IdentityKeys({ + signalProtocolStore, + ourServiceId: ourAci, + }); + const sessionStore = new Sessions({ + signalProtocolStore, + ourServiceId: ourAci, + }); return sealedSenderMultiRecipientEncrypt( content, recipients,