diff --git a/lib/types/sqlite-types.js b/lib/types/sqlite-types.js index ad4cd382e..17eeb723a 100644 --- a/lib/types/sqlite-types.js +++ b/lib/types/sqlite-types.js @@ -1,82 +1,85 @@ // @flow import type { ClientDBMessageInfo } from './message-types.js'; import type { StoreOperations } from './store-ops-types.js'; export const outboundP2PMessageStatuses = Object.freeze({ // The message was prepared to be sent to other peers, but it's not encrypted. // It was inserted into DB in the same transaction as making changes to // the store. persisted: 'persisted', // Encryption is done in the same transaction as persisting the CryptoModule, // and message order is also tracked on the client side, // which means the message can be sent. encrypted: 'encrypted', // The message was sent to another peer (Tunnelbroker owns it), // waiting for the confirmation (handled in `peerToPeerMessageHandler`). sent: 'sent', }); export type OutboundP2PMessageStatuses = $Values< typeof outboundP2PMessageStatuses, >; export type InboundP2PMessage = { +messageID: string, +senderDeviceID: string, +plaintext: string, +status: string, +senderUserID: string, }; export type OutboundP2PMessage = { +messageID: string, +deviceID: string, +userID: string, +timestamp: string, +plaintext: string, +ciphertext: string, +status: OutboundP2PMessageStatuses, +supportsAutoRetry: boolean, }; export type SQLiteAPI = { // read operations +getAllInboundP2PMessages: () => Promise>, + +getInboundP2PMessagesByID: ( + ids: $ReadOnlyArray, + ) => Promise>, +getAllOutboundP2PMessages: () => Promise>, +getRelatedMessages: ( messageID: string, ) => Promise>, +getOutboundP2PMessagesByID: ( ids: $ReadOnlyArray, ) => Promise>, +searchMessages: ( query: string, threadID: string, timestampCursor: ?string, messageIDCursor: ?string, ) => Promise>, +fetchMessages: ( threadID: string, limit: number, offset: number, ) => Promise>, // write operations +removeInboundP2PMessages: (ids: $ReadOnlyArray) => Promise, +markOutboundP2PMessageAsSent: ( messageID: string, deviceID: string, ) => Promise, +resetOutboundP2PMessagesForDevice: ( deviceID: string, ) => Promise>, +removeOutboundP2PMessage: ( messageID: string, deviceID: string, ) => Promise, +processDBStoreOperations: ( operations: StoreOperations, userID?: ?string, ) => Promise, }; diff --git a/lib/utils/__mocks__/config.js b/lib/utils/__mocks__/config.js index aee256013..81cee4881 100644 --- a/lib/utils/__mocks__/config.js +++ b/lib/utils/__mocks__/config.js @@ -1,64 +1,65 @@ // @flow import { type Config } from '../config.js'; const getConfig = (): Config => ({ resolveKeyserverSessionInvalidationUsingNativeCredentials: null, setSessionIDOnRequest: true, calendarRangeInactivityLimit: null, platformDetails: { platform: 'web', codeVersion: 70, stateVersion: 50, }, authoritativeKeyserverID: '123', olmAPI: { initializeCryptoAccount: jest.fn(), getUserPublicKey: jest.fn(), encrypt: jest.fn(), encryptAndPersist: jest.fn(), encryptNotification: jest.fn(), decrypt: jest.fn(), decryptAndPersist: jest.fn(), contentInboundSessionCreator: jest.fn(), contentOutboundSessionCreator: jest.fn(), keyserverNotificationsSessionCreator: jest.fn(), notificationsOutboundSessionCreator: jest.fn(), isContentSessionInitialized: jest.fn(), isDeviceNotificationsSessionInitialized: jest.fn(), isNotificationsSessionInitializedWithDevices: jest.fn(), getOneTimeKeys: jest.fn(), validateAndUploadPrekeys: jest.fn(), signMessage: jest.fn(), verifyMessage: jest.fn(), markPrekeysAsPublished: jest.fn(), }, sqliteAPI: { getAllInboundP2PMessages: jest.fn(), + getInboundP2PMessagesByID: jest.fn(), removeInboundP2PMessages: jest.fn(), processDBStoreOperations: jest.fn(), getAllOutboundP2PMessages: jest.fn(), markOutboundP2PMessageAsSent: jest.fn(), removeOutboundP2PMessage: jest.fn(), resetOutboundP2PMessagesForDevice: jest.fn(), getRelatedMessages: jest.fn(), getOutboundP2PMessagesByID: jest.fn(), searchMessages: jest.fn(), fetchMessages: jest.fn(), }, encryptedNotifUtilsAPI: { generateAESKey: jest.fn(), encryptWithAESKey: jest.fn(), encryptSerializedNotifPayload: jest.fn(), uploadLargeNotifPayload: jest.fn(), getEncryptedNotifHash: jest.fn(), getBlobHash: jest.fn(), getNotifByteSize: jest.fn(), normalizeUint8ArrayForBlobUpload: jest.fn(), }, showAlert: jest.fn(), }); const hasConfig = (): boolean => true; export { getConfig, hasConfig }; diff --git a/native/database/sqlite-api.js b/native/database/sqlite-api.js index 3ed801ec0..321a73586 100644 --- a/native/database/sqlite-api.js +++ b/native/database/sqlite-api.js @@ -1,64 +1,65 @@ // @flow import { getKeyserversToRemoveFromNotifsStore } from 'lib/ops/keyserver-store-ops.js'; import { convertStoreOperationsToClientDBStoreOperations } from 'lib/shared/redux/client-db-utils.js'; import type { SQLiteAPI } from 'lib/types/sqlite-types.js'; import type { StoreOperations } from 'lib/types/store-ops-types'; import { values } from 'lib/utils/objects.js'; import { commCoreModule } from '../native-modules.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; const sqliteAPI: SQLiteAPI = { // read operations getAllInboundP2PMessages: commCoreModule.getAllInboundP2PMessages, + getInboundP2PMessagesByID: commCoreModule.getInboundP2PMessagesByID, getAllOutboundP2PMessages: commCoreModule.getAllOutboundP2PMessages, getRelatedMessages: commCoreModule.getRelatedMessages, getOutboundP2PMessagesByID: commCoreModule.getOutboundP2PMessagesByID, searchMessages: commCoreModule.searchMessages, fetchMessages: commCoreModule.fetchMessages, // write operations removeInboundP2PMessages: commCoreModule.removeInboundP2PMessages, markOutboundP2PMessageAsSent: commCoreModule.markOutboundP2PMessageAsSent, resetOutboundP2PMessagesForDevice: commCoreModule.resetOutboundP2PMessagesForDevice, removeOutboundP2PMessage: commCoreModule.removeOutboundP2PMessage, async processDBStoreOperations( storeOperations: StoreOperations, ): Promise { const keyserversToRemoveFromNotifsStore = getKeyserversToRemoveFromNotifsStore( storeOperations.keyserverStoreOperations ?? [], ); try { const promises = []; if (keyserversToRemoveFromNotifsStore.length > 0) { promises.push( commCoreModule.removeKeyserverDataFromNotifStorage( keyserversToRemoveFromNotifsStore, ), ); } const dbOps = convertStoreOperationsToClientDBStoreOperations(storeOperations); if (values(dbOps).some(ops => ops && ops.length > 0)) { promises.push(commCoreModule.processDBStoreOperations(dbOps)); } await Promise.all(promises); } catch (e) { if (isTaskCancelledError(e)) { return; } // this code will make an entry in SecureStore and cause re-creating // database when user will open app again commCoreModule.reportDBOperationsFailure(); commCoreModule.terminate(); } }, }; export { sqliteAPI }; diff --git a/web/database/sqlite-api.js b/web/database/sqlite-api.js index 0fea3d6ca..d3e51d946 100644 --- a/web/database/sqlite-api.js +++ b/web/database/sqlite-api.js @@ -1,187 +1,201 @@ // @flow import { convertStoreOperationsToClientDBStoreOperations } from 'lib/shared/redux/client-db-utils.js'; import type { ClientDBMessageInfo } from 'lib/types/message-types.js'; import type { SQLiteAPI, InboundP2PMessage, OutboundP2PMessage, } from 'lib/types/sqlite-types.js'; import type { StoreOperations } from 'lib/types/store-ops-types.js'; import { entries, values } from 'lib/utils/objects.js'; import { getCommSharedWorker } from '../shared-worker/shared-worker-provider.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; const sqliteAPI: SQLiteAPI = { // read operations async getAllInboundP2PMessages(): Promise { const sharedWorker = await getCommSharedWorker(); const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_INBOUND_P2P_MESSAGES, }); const messages: ?$ReadOnlyArray = data?.inboundP2PMessages; return messages ? [...messages] : []; }, + async getInboundP2PMessagesByID( + ids: $ReadOnlyArray, + ): Promise> { + const sharedWorker = await getCommSharedWorker(); + + const data = await sharedWorker.schedule({ + type: workerRequestMessageTypes.GET_INBOUND_P2P_MESSAGES_BY_ID, + messageIDs: ids, + }); + const messages: ?$ReadOnlyArray = + data?.inboundP2PMessages; + return messages ? [...messages] : []; + }, + async getAllOutboundP2PMessages(): Promise { const sharedWorker = await getCommSharedWorker(); const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_OUTBOUND_P2P_MESSAGES, }); const messages: ?$ReadOnlyArray = data?.outboundP2PMessages; return messages ? [...messages] : []; }, async getRelatedMessages(messageID: string): Promise { const sharedWorker = await getCommSharedWorker(); const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_RELATED_MESSAGES, messageID, }); const messages: ?$ReadOnlyArray = data?.messages; return messages ? [...messages] : []; }, async getOutboundP2PMessagesByID( ids: $ReadOnlyArray, ): Promise> { const sharedWorker = await getCommSharedWorker(); const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_OUTBOUND_P2P_MESSAGES_BY_ID, messageIDs: ids, }); const messages: ?$ReadOnlyArray = data?.outboundP2PMessages; return messages ? [...messages] : []; }, async searchMessages( query: string, threadID: string, timestampCursor: ?string, messageIDCursor: ?string, ): Promise { const sharedWorker = await getCommSharedWorker(); const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.SEARCH_MESSAGES, query, threadID, timestampCursor, messageIDCursor, }); const messages: ?$ReadOnlyArray = data?.messages; return messages ? [...messages] : []; }, async fetchMessages( threadID: string, limit: number, offset: number, ): Promise { const sharedWorker = await getCommSharedWorker(); const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.FETCH_MESSAGES, threadID, limit, offset, }); const messages: ?$ReadOnlyArray = data?.messages; return messages ? [...messages] : []; }, // write operations async removeInboundP2PMessages(ids: $ReadOnlyArray): Promise { const sharedWorker = await getCommSharedWorker(); await sharedWorker.schedule({ type: workerRequestMessageTypes.REMOVE_INBOUND_P2P_MESSAGES, ids, }); }, async markOutboundP2PMessageAsSent( messageID: string, deviceID: string, ): Promise { const sharedWorker = await getCommSharedWorker(); await sharedWorker.schedule({ type: workerRequestMessageTypes.MARK_OUTBOUND_P2P_MESSAGE_AS_SENT, messageID, deviceID, }); }, async resetOutboundP2PMessagesForDevice( deviceID: string, ): Promise> { const sharedWorker = await getCommSharedWorker(); const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.RESET_OUTBOUND_P2P_MESSAGES, deviceID, }); const messageIDs: ?$ReadOnlyArray = data?.messageIDs; return messageIDs ? [...messageIDs] : []; }, async removeOutboundP2PMessage( messageID: string, deviceID: string, ): Promise { const sharedWorker = await getCommSharedWorker(); await sharedWorker.schedule({ type: workerRequestMessageTypes.REMOVE_OUTBOUND_P2P_MESSAGE, messageID, deviceID, }); }, async processDBStoreOperations( storeOperations: StoreOperations, ): Promise { const dbOps = convertStoreOperationsToClientDBStoreOperations(storeOperations); if (!values(dbOps).some(ops => ops && ops.length > 0)) { return; } const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return; } try { await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: dbOps, }); } catch (e) { console.log(e); if ( entries(storeOperations).some( ([key, ops]) => key !== 'draftStoreOperations' && key !== 'reportStoreOperations' && ops.length > 0, ) ) { await sharedWorker.init({ clearDatabase: true, markAsCorrupted: true }); location.reload(); } } }, }; export { sqliteAPI };