diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h index 3e98a3f84..fc96cd5f7 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h @@ -1,66 +1,67 @@ #pragma once #include "../CryptoTools/Persist.h" #include "DatabaseQueryExecutor.h" #include "entities/Draft.h" #include #include namespace comm { class SQLiteQueryExecutor : public DatabaseQueryExecutor { void migrate() const; static void assign_encryption_key(); static auto &getStorage(); static std::once_flag initialized; static int sqlcipherEncryptionKeySize; - static std::string secureStoreEncryptionKeyID; public: static std::string sqliteFilePath; static std::string encryptionKey; + static std::string secureStoreEncryptionKeyID; + SQLiteQueryExecutor(); static void initialize(std::string &databasePath); std::unique_ptr getThread(std::string threadID) const override; std::string getDraft(std::string key) const override; void updateDraft(std::string key, std::string text) const override; bool moveDraft(std::string oldKey, std::string newKey) const override; std::vector getAllDrafts() const override; void removeAllDrafts() const override; void removeAllMessages() const override; std::vector>> getAllMessages() const override; void removeMessages(const std::vector &ids) const override; void removeMessagesForThreads( const std::vector &threadIDs) const override; void replaceMessage(const Message &message) const override; void rekeyMessage(std::string from, std::string to) const override; void removeAllMedia() const override; void removeMediaForMessages( const std::vector &msg_ids) const override; void removeMediaForMessage(std::string msg_id) const override; void removeMediaForThreads( const std::vector &thread_ids) const override; void replaceMedia(const Media &media) const override; void rekeyMediaContainers(std::string from, std::string to) const override; std::vector getAllThreads() const override; void removeThreads(std::vector ids) const override; void replaceThread(const Thread &thread) const override; void removeAllThreads() const override; void beginTransaction() const override; void commitTransaction() const override; void rollbackTransaction() const override; std::vector getOlmPersistSessionsData() const override; folly::Optional getOlmPersistAccountData() const override; void storeOlmPersistData(crypto::Persist persist) const override; void setNotifyToken(std::string token) const override; void clearNotifyToken() const override; void setCurrentUserID(std::string userID) const override; std::string getCurrentUserID() const override; void clearSensitiveData() const override; }; } // namespace comm diff --git a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp index a88211f5a..a451d6271 100644 --- a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp +++ b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp @@ -1,1026 +1,1044 @@ #include "CommCoreModule.h" #include "DatabaseManager.h" #include "GRPCStreamHostObject.h" #include "InternalModules/GlobalNetworkSingleton.h" #include "InternalModules/NetworkModule.h" #include "Logger.h" #include "MessageStoreOperations.h" #include "ThreadStoreOperations.h" #include #include "../DatabaseManagers/entities/Media.h" #include #include namespace comm { using namespace facebook::react; +template +T CommCoreModule::runSyncOrThrowJSError( + jsi::Runtime &rt, + std::function task) { + std::promise promise; + this->databaseThread->scheduleTask([&promise, &task]() { + try { + promise.set_value(task()); + } catch (const std::exception &e) { + promise.set_exception(std::make_exception_ptr(e)); + } + }); + // We cannot instantiate JSError on database thread, so + // on the main thread we re-throw C++ error, catch it and + // transform to informative JSError on the main thread + try { + return promise.get_future().get(); + } catch (const std::exception &e) { + throw jsi::JSError(rt, e.what()); + } +} + jsi::Value CommCoreModule::getDraft(jsi::Runtime &rt, const jsi::String &key) { std::string keyStr = key.utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string draftStr; try { draftStr = DatabaseManager::getQueryExecutor().getDraft(keyStr); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } jsi::String draft = jsi::String::createFromUtf8(innerRt, draftStr); promise->resolve(std::move(draft)); }); }; this->databaseThread->scheduleTask(job); }); } jsi::Value CommCoreModule::updateDraft(jsi::Runtime &rt, const jsi::Object &draft) { std::string keyStr = draft.getProperty(rt, "key").asString(rt).utf8(rt); std::string textStr = draft.getProperty(rt, "text").asString(rt).utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=]() { std::string error; try { DatabaseManager::getQueryExecutor().updateDraft(keyStr, textStr); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); } else { promise->resolve(true); } }); }; this->databaseThread->scheduleTask(job); }); } jsi::Value CommCoreModule::moveDraft( jsi::Runtime &rt, const jsi::String &oldKey, const jsi::String &newKey) { std::string oldKeyStr = oldKey.utf8(rt); std::string newKeyStr = newKey.utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=]() { std::string error; bool result = false; try { result = DatabaseManager::getQueryExecutor().moveDraft( oldKeyStr, newKeyStr); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); } else { promise->resolve(result); } }); }; this->databaseThread->scheduleTask(job); }); } jsi::Value CommCoreModule::getAllDrafts(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::vector draftsVector; size_t numDrafts; try { draftsVector = DatabaseManager::getQueryExecutor().getAllDrafts(); numDrafts = count_if( draftsVector.begin(), draftsVector.end(), [](Draft draft) { return !draft.text.empty(); }); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } jsi::Array jsiDrafts = jsi::Array(innerRt, numDrafts); size_t writeIndex = 0; for (Draft draft : draftsVector) { if (draft.text.empty()) { continue; } auto jsiDraft = jsi::Object(innerRt); jsiDraft.setProperty(innerRt, "key", draft.key); jsiDraft.setProperty(innerRt, "text", draft.text); jsiDrafts.setValueAtIndex(innerRt, writeIndex++, jsiDraft); } promise->resolve(std::move(jsiDrafts)); }); }; this->databaseThread->scheduleTask(job); }); } jsi::Value CommCoreModule::removeAllDrafts(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=]() { std::string error; try { DatabaseManager::getQueryExecutor().removeAllDrafts(); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); return; } promise->resolve(jsi::Value::undefined()); }); }; this->databaseThread->scheduleTask(job); }); } jsi::Array CommCoreModule::getAllMessagesSync(jsi::Runtime &rt) { - std::promise>>> - messagesResult; - auto messagesResultFuture = messagesResult.get_future(); - - this->databaseThread->scheduleTask([&messagesResult]() { - messagesResult.set_value( - DatabaseManager::getQueryExecutor().getAllMessages()); + auto messagesVector = this->runSyncOrThrowJSError< + std::vector>>>(rt, []() { + return DatabaseManager::getQueryExecutor().getAllMessages(); }); - - auto messagesVector = messagesResultFuture.get(); size_t numMessages{messagesVector.size()}; jsi::Array jsiMessages = jsi::Array(rt, numMessages); size_t writeIndex = 0; for (const auto &[message, media] : messagesVector) { auto jsiMessage = jsi::Object(rt); jsiMessage.setProperty(rt, "id", message.id); if (message.local_id) { auto local_id = message.local_id.get(); jsiMessage.setProperty(rt, "local_id", *local_id); } jsiMessage.setProperty(rt, "thread", message.thread); jsiMessage.setProperty(rt, "user", message.user); jsiMessage.setProperty(rt, "type", std::to_string(message.type)); if (message.future_type) { auto future_type = message.future_type.get(); jsiMessage.setProperty(rt, "future_type", std::to_string(*future_type)); } if (message.content) { auto content = message.content.get(); jsiMessage.setProperty(rt, "content", *content); } jsiMessage.setProperty(rt, "time", std::to_string(message.time)); size_t media_idx = 0; jsi::Array jsiMediaArray = jsi::Array(rt, media.size()); for (const auto &media_info : media) { auto jsiMedia = jsi::Object(rt); jsiMedia.setProperty(rt, "id", media_info.id); jsiMedia.setProperty(rt, "uri", media_info.uri); jsiMedia.setProperty(rt, "type", media_info.type); jsiMedia.setProperty(rt, "extras", media_info.extras); jsiMediaArray.setValueAtIndex(rt, media_idx++, jsiMedia); } jsiMessage.setProperty(rt, "media_infos", jsiMediaArray); jsiMessages.setValueAtIndex(rt, writeIndex++, jsiMessage); } return jsiMessages; } jsi::Value CommCoreModule::getAllMessages(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::vector>> messagesVector; size_t numMessages; try { messagesVector = DatabaseManager::getQueryExecutor().getAllMessages(); numMessages = messagesVector.size(); } catch (std::system_error &e) { error = e.what(); } auto messagesVectorPtr = std::make_shared< std::vector>>>( std::move(messagesVector)); this->jsInvoker_->invokeAsync( [messagesVectorPtr, &innerRt, promise, error, numMessages]() { if (error.size()) { promise->reject(error); return; } jsi::Array jsiMessages = jsi::Array(innerRt, numMessages); size_t writeIndex = 0; for (const auto &[message, media] : *messagesVectorPtr) { auto jsiMessage = jsi::Object(innerRt); jsiMessage.setProperty(innerRt, "id", message.id); if (message.local_id) { auto local_id = message.local_id.get(); jsiMessage.setProperty(innerRt, "local_id", *local_id); } jsiMessage.setProperty(innerRt, "thread", message.thread); jsiMessage.setProperty(innerRt, "user", message.user); jsiMessage.setProperty( innerRt, "type", std::to_string(message.type)); if (message.future_type) { auto future_type = message.future_type.get(); jsiMessage.setProperty( innerRt, "future_type", std::to_string(*future_type)); } if (message.content) { auto content = message.content.get(); jsiMessage.setProperty(innerRt, "content", *content); } jsiMessage.setProperty( innerRt, "time", std::to_string(message.time)); size_t media_idx = 0; jsi::Array jsiMediaArray = jsi::Array(innerRt, media.size()); for (const auto &media_info : media) { auto jsiMedia = jsi::Object(innerRt); jsiMedia.setProperty(innerRt, "id", media_info.id); jsiMedia.setProperty(innerRt, "uri", media_info.uri); jsiMedia.setProperty(innerRt, "type", media_info.type); jsiMedia.setProperty(innerRt, "extras", media_info.extras); jsiMediaArray.setValueAtIndex( innerRt, media_idx++, jsiMedia); } jsiMessage.setProperty(innerRt, "media_infos", jsiMediaArray); jsiMessages.setValueAtIndex( innerRt, writeIndex++, jsiMessage); } promise->resolve(std::move(jsiMessages)); }); }; this->databaseThread->scheduleTask(job); }); } #define REKEY_OPERATION "rekey" #define REMOVE_OPERATION "remove" #define REPLACE_OPERATION "replace" #define REMOVE_MSGS_FOR_THREADS_OPERATION "remove_messages_for_threads" #define REMOVE_ALL_OPERATION "remove_all" std::vector> createMessageStoreOperations(jsi::Runtime &rt, const jsi::Array &operations) { std::vector> messageStoreOps; for (auto idx = 0; idx < operations.size(rt); idx++) { auto op = operations.getValueAtIndex(rt, idx).asObject(rt); auto op_type = op.getProperty(rt, "type").asString(rt).utf8(rt); if (op_type == REMOVE_ALL_OPERATION) { messageStoreOps.push_back(std::make_unique()); continue; } auto payload_obj = op.getProperty(rt, "payload").asObject(rt); if (op_type == REMOVE_OPERATION) { messageStoreOps.push_back( std::make_unique(rt, payload_obj)); } else if (op_type == REMOVE_MSGS_FOR_THREADS_OPERATION) { messageStoreOps.push_back( std::make_unique(rt, payload_obj)); } else if (op_type == REPLACE_OPERATION) { messageStoreOps.push_back( std::make_unique(rt, payload_obj)); } else if (op_type == REKEY_OPERATION) { messageStoreOps.push_back( std::make_unique(rt, payload_obj)); } else { throw std::runtime_error("unsupported operation: " + op_type); } } return messageStoreOps; } jsi::Value CommCoreModule::processMessageStoreOperations( jsi::Runtime &rt, const jsi::Array &operations) { std::string createOperationsError; std::shared_ptr>> messageStoreOpsPtr; try { auto messageStoreOps = createMessageStoreOperations(rt, operations); messageStoreOpsPtr = std::make_shared< std::vector>>( std::move(messageStoreOps)); } catch (std::runtime_error &e) { createOperationsError = e.what(); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=]() { std::string error = createOperationsError; if (!error.size()) { try { DatabaseManager::getQueryExecutor().beginTransaction(); for (const auto &operation : *messageStoreOpsPtr) { operation->execute(); } DatabaseManager::getQueryExecutor().commitTransaction(); } catch (std::system_error &e) { error = e.what(); DatabaseManager::getQueryExecutor().rollbackTransaction(); } } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }; this->databaseThread->scheduleTask(job); }); } bool CommCoreModule::processMessageStoreOperationsSync( jsi::Runtime &rt, const jsi::Array &operations) { std::promise operationsResult; std::future operationsResultFuture = operationsResult.get_future(); std::vector> messageStoreOps; std::string operationsError; try { messageStoreOps = createMessageStoreOperations(rt, operations); } catch (std::runtime_error &e) { return false; } this->databaseThread->scheduleTask( [=, &messageStoreOps, &operationsResult, &rt]() { std::string error = operationsError; if (!error.size()) { try { DatabaseManager::getQueryExecutor().beginTransaction(); for (const auto &operation : messageStoreOps) { operation->execute(); } DatabaseManager::getQueryExecutor().commitTransaction(); } catch (std::system_error &e) { error = e.what(); DatabaseManager::getQueryExecutor().rollbackTransaction(); } } operationsResult.set_value(error.size() == 0); }); return operationsResultFuture.get(); } jsi::Value CommCoreModule::getAllThreads(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { this->databaseThread->scheduleTask([=, &innerRt]() { std::string error; std::vector threadsVector; size_t numThreads; try { threadsVector = DatabaseManager::getQueryExecutor().getAllThreads(); numThreads = threadsVector.size(); } catch (std::system_error &e) { error = e.what(); } auto threadsVectorPtr = std::make_shared>(std::move(threadsVector)); this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } jsi::Array jsiThreads = jsi::Array(innerRt, numThreads); size_t writeIdx = 0; for (const Thread &thread : *threadsVectorPtr) { jsi::Object jsiThread = jsi::Object(innerRt); jsiThread.setProperty(innerRt, "id", thread.id); jsiThread.setProperty(innerRt, "type", thread.type); jsiThread.setProperty( innerRt, "name", thread.name ? jsi::String::createFromUtf8(innerRt, *thread.name) : jsi::Value::null()); jsiThread.setProperty( innerRt, "description", thread.description ? jsi::String::createFromUtf8( innerRt, *thread.description) : jsi::Value::null()); jsiThread.setProperty(innerRt, "color", thread.color); jsiThread.setProperty( innerRt, "creationTime", std::to_string(thread.creation_time)); jsiThread.setProperty( innerRt, "parentThreadID", thread.parent_thread_id ? jsi::String::createFromUtf8( innerRt, *thread.parent_thread_id) : jsi::Value::null()); jsiThread.setProperty( innerRt, "containingThreadID", thread.containing_thread_id ? jsi::String::createFromUtf8( innerRt, *thread.containing_thread_id) : jsi::Value::null()); jsiThread.setProperty( innerRt, "community", thread.community ? jsi::String::createFromUtf8(innerRt, *thread.community) : jsi::Value::null()); jsiThread.setProperty(innerRt, "members", thread.members); jsiThread.setProperty(innerRt, "roles", thread.roles); jsiThread.setProperty( innerRt, "currentUser", thread.current_user); jsiThread.setProperty( innerRt, "sourceMessageID", thread.source_message_id ? jsi::String::createFromUtf8( innerRt, *thread.source_message_id) : jsi::Value::null()); jsiThread.setProperty( innerRt, "repliesCount", thread.replies_count); jsiThreads.setValueAtIndex(innerRt, writeIdx++, jsiThread); } promise->resolve(std::move(jsiThreads)); }); }); }); }; jsi::Array CommCoreModule::getAllThreadsSync(jsi::Runtime &rt) { - std::promise> threadsResult; - auto threadsResultFuture = threadsResult.get_future(); - - this->databaseThread->scheduleTask([&threadsResult]() { - threadsResult.set_value( - DatabaseManager::getQueryExecutor().getAllThreads()); - }); - - auto threadsVector = threadsResultFuture.get(); + auto threadsVector = this->runSyncOrThrowJSError>( + rt, []() { return DatabaseManager::getQueryExecutor().getAllThreads(); }); size_t numThreads{threadsVector.size()}; jsi::Array jsiThreads = jsi::Array(rt, numThreads); size_t writeIdx = 0; for (const Thread &thread : threadsVector) { jsi::Object jsiThread = jsi::Object(rt); jsiThread.setProperty(rt, "id", thread.id); jsiThread.setProperty(rt, "type", thread.type); jsiThread.setProperty( rt, "name", thread.name ? jsi::String::createFromUtf8(rt, *thread.name) : jsi::Value::null()); jsiThread.setProperty( rt, "description", thread.description ? jsi::String::createFromUtf8(rt, *thread.description) : jsi::Value::null()); jsiThread.setProperty(rt, "color", thread.color); jsiThread.setProperty( rt, "creationTime", std::to_string(thread.creation_time)); jsiThread.setProperty( rt, "parentThreadID", thread.parent_thread_id ? jsi::String::createFromUtf8(rt, *thread.parent_thread_id) : jsi::Value::null()); jsiThread.setProperty( rt, "containingThreadID", thread.containing_thread_id ? jsi::String::createFromUtf8(rt, *thread.containing_thread_id) : jsi::Value::null()); jsiThread.setProperty( rt, "community", thread.community ? jsi::String::createFromUtf8(rt, *thread.community) : jsi::Value::null()); jsiThread.setProperty(rt, "members", thread.members); jsiThread.setProperty(rt, "roles", thread.roles); jsiThread.setProperty(rt, "currentUser", thread.current_user); jsiThread.setProperty( rt, "sourceMessageID", thread.source_message_id ? jsi::String::createFromUtf8(rt, *thread.source_message_id) : jsi::Value::null()); jsiThread.setProperty(rt, "repliesCount", thread.replies_count); jsiThreads.setValueAtIndex(rt, writeIdx++, jsiThread); } return jsiThreads; } std::vector> createThreadStoreOperations(jsi::Runtime &rt, const jsi::Array &operations) { std::vector> threadStoreOps; for (size_t idx = 0; idx < operations.size(rt); idx++) { jsi::Object op = operations.getValueAtIndex(rt, idx).asObject(rt); std::string opType = op.getProperty(rt, "type").asString(rt).utf8(rt); if (opType == REMOVE_OPERATION) { std::vector threadIDsToRemove; jsi::Object payloadObj = op.getProperty(rt, "payload").asObject(rt); jsi::Array threadIDs = payloadObj.getProperty(rt, "ids").asObject(rt).asArray(rt); for (int threadIdx = 0; threadIdx < threadIDs.size(rt); threadIdx++) { threadIDsToRemove.push_back( threadIDs.getValueAtIndex(rt, threadIdx).asString(rt).utf8(rt)); } threadStoreOps.push_back(std::make_unique( std::move(threadIDsToRemove))); } else if (opType == REMOVE_ALL_OPERATION) { threadStoreOps.push_back(std::make_unique()); } else if (opType == REPLACE_OPERATION) { jsi::Object threadObj = op.getProperty(rt, "payload").asObject(rt); std::string threadID = threadObj.getProperty(rt, "id").asString(rt).utf8(rt); int type = std::lround(threadObj.getProperty(rt, "type").asNumber()); jsi::Value maybeName = threadObj.getProperty(rt, "name"); std::unique_ptr name = maybeName.isString() ? std::make_unique(maybeName.asString(rt).utf8(rt)) : nullptr; jsi::Value maybeDescription = threadObj.getProperty(rt, "description"); std::unique_ptr description = maybeDescription.isString() ? std::make_unique( maybeDescription.asString(rt).utf8(rt)) : nullptr; std::string color = threadObj.getProperty(rt, "color").asString(rt).utf8(rt); int64_t creationTime = std::stoll( threadObj.getProperty(rt, "creationTime").asString(rt).utf8(rt)); jsi::Value maybeParentThreadID = threadObj.getProperty(rt, "parentThreadID"); std::unique_ptr parentThreadID = maybeParentThreadID.isString() ? std::make_unique( maybeParentThreadID.asString(rt).utf8(rt)) : nullptr; jsi::Value maybeContainingThreadID = threadObj.getProperty(rt, "containingThreadID"); std::unique_ptr containingThreadID = maybeContainingThreadID.isString() ? std::make_unique( maybeContainingThreadID.asString(rt).utf8(rt)) : nullptr; jsi::Value maybeCommunity = threadObj.getProperty(rt, "community"); std::unique_ptr community = maybeCommunity.isString() ? std::make_unique(maybeCommunity.asString(rt).utf8(rt)) : nullptr; std::string members = threadObj.getProperty(rt, "members").asString(rt).utf8(rt); std::string roles = threadObj.getProperty(rt, "roles").asString(rt).utf8(rt); std::string currentUser = threadObj.getProperty(rt, "currentUser").asString(rt).utf8(rt); jsi::Value maybeSourceMessageID = threadObj.getProperty(rt, "sourceMessageID"); std::unique_ptr sourceMessageID = maybeSourceMessageID.isString() ? std::make_unique( maybeSourceMessageID.asString(rt).utf8(rt)) : nullptr; int repliesCount = std::lround(threadObj.getProperty(rt, "repliesCount").asNumber()); Thread thread{ threadID, type, std::move(name), std::move(description), color, creationTime, std::move(parentThreadID), std::move(containingThreadID), std::move(community), members, roles, currentUser, std::move(sourceMessageID), repliesCount}; threadStoreOps.push_back( std::make_unique(std::move(thread))); } else { throw std::runtime_error("unsupported operation: " + opType); } }; return threadStoreOps; } jsi::Value CommCoreModule::processThreadStoreOperations( jsi::Runtime &rt, const jsi::Array &operations) { std::string operationsError; std::shared_ptr>> threadStoreOpsPtr; try { auto threadStoreOps = createThreadStoreOperations(rt, operations); threadStoreOpsPtr = std::make_shared< std::vector>>( std::move(threadStoreOps)); } catch (std::runtime_error &e) { operationsError = e.what(); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { this->databaseThread->scheduleTask([=]() { std::string error = operationsError; if (!error.size()) { try { DatabaseManager::getQueryExecutor().beginTransaction(); for (const auto &operation : *threadStoreOpsPtr) { operation->execute(); } DatabaseManager::getQueryExecutor().commitTransaction(); } catch (std::system_error &e) { error = e.what(); DatabaseManager::getQueryExecutor().rollbackTransaction(); } } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }); }); } bool CommCoreModule::processThreadStoreOperationsSync( jsi::Runtime &rt, const jsi::Array &operations) { std::promise operationsResult; std::future operationsResultFuture = operationsResult.get_future(); std::vector> threadStoreOps; std::string operationsError; try { threadStoreOps = createThreadStoreOperations(rt, operations); } catch (std::runtime_error &e) { return false; } this->databaseThread->scheduleTask( [=, &threadStoreOps, &operationsResult, &rt]() { std::string error = operationsError; if (!error.size()) { try { DatabaseManager::getQueryExecutor().beginTransaction(); for (const auto &operation : threadStoreOps) { operation->execute(); } DatabaseManager::getQueryExecutor().commitTransaction(); } catch (std::system_error &e) { error = e.what(); DatabaseManager::getQueryExecutor().rollbackTransaction(); } } operationsResult.set_value(error.size() == 0); }); return operationsResultFuture.get(); } jsi::Value CommCoreModule::initializeCryptoAccount( jsi::Runtime &rt, const jsi::String &userId) { std::string userIdStr = userId.utf8(rt); folly::Optional storedSecretKey = this->secureStore.get(this->secureStoreAccountDataKey); if (!storedSecretKey.hasValue()) { storedSecretKey = crypto::Tools::generateRandomString(64); this->secureStore.set( this->secureStoreAccountDataKey, storedSecretKey.value()); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { this->databaseThread->scheduleTask([=]() { crypto::Persist persist; std::string error; try { folly::Optional accountData = DatabaseManager::getQueryExecutor().getOlmPersistAccountData(); if (accountData.hasValue()) { persist.account = crypto::OlmBuffer(accountData->begin(), accountData->end()); // handle sessions data std::vector sessionsData = DatabaseManager::getQueryExecutor() .getOlmPersistSessionsData(); for (OlmPersistSession &sessionsDataItem : sessionsData) { crypto::OlmBuffer sessionDataBuffer( sessionsDataItem.session_data.begin(), sessionsDataItem.session_data.end()); persist.sessions.insert(std::make_pair( sessionsDataItem.target_user_id, sessionDataBuffer)); } } } catch (std::system_error &e) { error = e.what(); } this->cryptoThread->scheduleTask([=]() { std::string error; this->cryptoModule.reset(new crypto::CryptoModule( userIdStr, storedSecretKey.value(), persist)); if (persist.isEmpty()) { crypto::Persist newPersist = this->cryptoModule->storeAsB64(storedSecretKey.value()); this->databaseThread->scheduleTask([=]() { std::string error; try { DatabaseManager::getQueryExecutor().storeOlmPersistData( newPersist); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); return; } promise->resolve(jsi::Value::undefined()); }); }); } else { this->cryptoModule->restoreFromB64( storedSecretKey.value(), persist); this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); return; } promise->resolve(jsi::Value::undefined()); }); } }); }); }); } void CommCoreModule::initializeNetworkModule( const std::string &userId, const std::string &deviceToken, const std::string &hostname) { GlobalNetworkSingleton::instance.scheduleOrRun( [=](NetworkModule &networkModule) { networkModule.initializeNetworkModule(userId, deviceToken, hostname); }); } jsi::Value CommCoreModule::getUserPublicKey(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string result; if (this->cryptoModule == nullptr) { error = "user has not been initialized"; } else { result = this->cryptoModule->getIdentityKeys(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve(jsi::String::createFromUtf8(innerRt, result)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::getUserOneTimeKeys(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string result; if (this->cryptoModule == nullptr) { error = "user has not been initialized"; } else { result = this->cryptoModule->getOneTimeKeys(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve(jsi::String::createFromUtf8(innerRt, result)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Object CommCoreModule::openSocket(jsi::Runtime &rt, const jsi::String &endpoint) { auto hostObject = std::make_shared(rt, this->jsInvoker_); return jsi::Object::createFromHostObject(rt, hostObject); } CommCoreModule::CommCoreModule( std::shared_ptr jsInvoker) : facebook::react::CommCoreModuleSchemaCxxSpecJSI(jsInvoker), databaseThread(std::make_unique("database")), cryptoThread(std::make_unique("crypto")) { GlobalNetworkSingleton::instance.enableMultithreading(); } +CommCoreModule::CommCoreModule( + std::shared_ptr jsInvoker, + std::shared_ptr databaseThread) + : facebook::react::CommCoreModuleSchemaCxxSpecJSI(jsInvoker), + databaseThread(databaseThread), + cryptoThread(std::make_unique("crypto")) { + GlobalNetworkSingleton::instance.enableMultithreading(); +} + double CommCoreModule::getCodeVersion(jsi::Runtime &rt) { return this->codeVersion; } jsi::Value CommCoreModule::setNotifyToken(jsi::Runtime &rt, const jsi::String &token) { auto notifyToken{token.utf8(rt)}; return createPromiseAsJSIValue( rt, [this, notifyToken](jsi::Runtime &innerRt, std::shared_ptr promise) { this->databaseThread->scheduleTask([this, notifyToken, promise]() { std::string error; try { DatabaseManager::getQueryExecutor().setNotifyToken(notifyToken); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }); }); } jsi::Value CommCoreModule::clearNotifyToken(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [this](jsi::Runtime &innerRt, std::shared_ptr promise) { this->databaseThread->scheduleTask([this, promise]() { std::string error; try { DatabaseManager::getQueryExecutor().clearNotifyToken(); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }); }); }; jsi::Value CommCoreModule::setCurrentUserID(jsi::Runtime &rt, const jsi::String &userID) { auto currentUserID{userID.utf8(rt)}; return createPromiseAsJSIValue( rt, [this, currentUserID](jsi::Runtime &innerRt, std::shared_ptr promise) { this->databaseThread->scheduleTask([this, promise, currentUserID]() { std::string error; try { DatabaseManager::getQueryExecutor().setCurrentUserID(currentUserID); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }); }); } jsi::Value CommCoreModule::getCurrentUserID(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [this](jsi::Runtime &innerRt, std::shared_ptr promise) { this->databaseThread->scheduleTask([this, &innerRt, promise]() { std::string error; std::string result; try { result = DatabaseManager::getQueryExecutor().getCurrentUserID(); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([&innerRt, error, result, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::String::createFromUtf8(innerRt, result)); } }); }); }); } jsi::Value CommCoreModule::clearSensitiveData(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [this](jsi::Runtime &innerRt, std::shared_ptr promise) { this->databaseThread->scheduleTask([this, promise]() { std::string error; try { DatabaseManager::getQueryExecutor().clearSensitiveData(); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }); }); } } // namespace comm diff --git a/native/cpp/CommonCpp/NativeModules/CommCoreModule.h b/native/cpp/CommonCpp/NativeModules/CommCoreModule.h index f6fb11cc1..f88f8c213 100644 --- a/native/cpp/CommonCpp/NativeModules/CommCoreModule.h +++ b/native/cpp/CommonCpp/NativeModules/CommCoreModule.h @@ -1,75 +1,80 @@ #pragma once #include "../CryptoTools/CryptoModule.h" #include "../Tools/CommSecureStore.h" #include "../Tools/WorkerThread.h" #include "../_generated/NativeModules.h" #include "../grpc/Client.h" #include #include namespace comm { namespace jsi = facebook::jsi; class CommCoreModule : public facebook::react::CommCoreModuleSchemaCxxSpecJSI { const int codeVersion{144}; - std::unique_ptr databaseThread; + std::shared_ptr databaseThread; std::unique_ptr cryptoThread; CommSecureStore secureStore; const std::string secureStoreAccountDataKey = "cryptoAccountDataKey"; std::unique_ptr cryptoModule; std::unique_ptr networkClient; + template + T runSyncOrThrowJSError(jsi::Runtime &rt, std::function task); jsi::Value getDraft(jsi::Runtime &rt, const jsi::String &key) override; jsi::Value updateDraft(jsi::Runtime &rt, const jsi::Object &draft) override; jsi::Value moveDraft( jsi::Runtime &rt, const jsi::String &oldKey, const jsi::String &newKey) override; jsi::Value getAllDrafts(jsi::Runtime &rt) override; jsi::Value removeAllDrafts(jsi::Runtime &rt) override; jsi::Value getAllMessages(jsi::Runtime &rt) override; jsi::Array getAllMessagesSync(jsi::Runtime &rt) override; jsi::Value processMessageStoreOperations( jsi::Runtime &rt, const jsi::Array &operations) override; bool processMessageStoreOperationsSync( jsi::Runtime &rt, const jsi::Array &operations) override; jsi::Value getAllThreads(jsi::Runtime &rt) override; jsi::Array getAllThreadsSync(jsi::Runtime &rt) override; jsi::Value processThreadStoreOperations( jsi::Runtime &rt, const jsi::Array &operations) override; bool processThreadStoreOperationsSync( jsi::Runtime &rt, const jsi::Array &operations) override; jsi::Value initializeCryptoAccount(jsi::Runtime &rt, const jsi::String &userId) override; jsi::Value getUserPublicKey(jsi::Runtime &rt) override; jsi::Value getUserOneTimeKeys(jsi::Runtime &rt) override; jsi::Object openSocket(jsi::Runtime &rt, const jsi::String &endpoint) override; double getCodeVersion(jsi::Runtime &rt) override; jsi::Value setNotifyToken(jsi::Runtime &rt, const jsi::String &token) override; jsi::Value clearNotifyToken(jsi::Runtime &rt) override; jsi::Value setCurrentUserID(jsi::Runtime &rt, const jsi::String &userID) override; jsi::Value getCurrentUserID(jsi::Runtime &rt) override; jsi::Value clearSensitiveData(jsi::Runtime &rt) override; public: CommCoreModule(std::shared_ptr jsInvoker); + CommCoreModule( + std::shared_ptr jsInvoker, + std::shared_ptr databaseThread); void initializeNetworkModule( const std::string &userId, const std::string &deviceToken, const std::string &hostname = ""); }; } // namespace comm diff --git a/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/ThreadOperationsUtilities/ThreadOperations.cpp b/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/ThreadOperationsUtilities/ThreadOperations.cpp index 8a218ee57..296e424e0 100644 --- a/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/ThreadOperationsUtilities/ThreadOperations.cpp +++ b/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/ThreadOperationsUtilities/ThreadOperations.cpp @@ -1,39 +1,40 @@ #include "ThreadOperations.h" #include "../../../DatabaseManagers/DatabaseManager.h" #include "Logger.h" #include #include #include namespace comm { void ThreadOperations::updateSQLiteUnreadStatus( std::string &threadID, bool unread) { std::unique_ptr thread = DatabaseManager::getQueryExecutor().getThread(threadID); if (thread == nullptr) { throw std::runtime_error( "Attempted to update non-existing thread with ID: " + threadID); } folly::dynamic updatedCurrentUser; try { updatedCurrentUser = folly::parseJson(thread->current_user); } catch (const folly::json::parse_error &e) { Logger::log( "Invalid json structure of current_user field of thread of id: " + threadID + ". Details: " + std::string(e.what())); return; } updatedCurrentUser["unread"] = unread; try { thread->current_user = folly::toJson(updatedCurrentUser); } catch (const folly::json::parse_error &e) { Logger::log( "Failed to serialize updated current_user JSON object. Details: " + std::string(e.what())); return; } - - DatabaseManager::getQueryExecutor().replaceThread(*thread); + // Line below is temporarily blocked to establish + // whether it is a reason for the crash + // DatabaseManager::getQueryExecutor().replaceThread(*thread); } } // namespace comm diff --git a/native/ios/Comm/AppDelegate.mm b/native/ios/Comm/AppDelegate.mm index 53c67126b..729ed60d2 100644 --- a/native/ios/Comm/AppDelegate.mm +++ b/native/ios/Comm/AppDelegate.mm @@ -1,301 +1,327 @@ #import "AppDelegate.h" #import "Orientation.h" #import "RNNotifications.h" #import #import #import #import #import #import #import #import #import #import #import "CommCoreModule.h" +#import "CommSecureStoreIOSWrapper.h" #import "GlobalNetworkSingleton.h" #import "Logger.h" #import "MessageOperationsUtilities.h" #import "NetworkModule.h" #import "SQLiteQueryExecutor.h" #import "TemporaryMessageStorage.h" #import "ThreadOperations.h" #import "Tools.h" +#import "WorkerThread.h" #import +#import #import #import #ifdef FB_SONARKIT_ENABLED #import #import #import #import #import #import static void InitializeFlipper(UIApplication *application) { FlipperClient *client = [FlipperClient sharedClient]; SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; [client addPlugin:[FlipperKitReactPlugin new]]; [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; [client start]; } #endif #import #import #import NSString *const backgroundNotificationTypeKey = @"backgroundNotifType"; NSString *const setUnreadStatusKey = @"setUnreadStatus"; @interface AppDelegate () < RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate> { } +@property(nonatomic, assign) std::shared_ptr + globalDatabaseThread; @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + NSString *secureStoreEncryptionKeyID = [NSString + stringWithUTF8String: + (comm::SQLiteQueryExecutor::secureStoreEncryptionKeyID.c_str())]; + [[CommSecureStoreIOSWrapper sharedInstance] + migrateOptionsForKey:secureStoreEncryptionKeyID + withVersion:@"0"]; [self attemptDatabaseInitialization]; + [self attemptDatabaseThreadInitialization]; return YES; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { #ifdef FB_SONARKIT_ENABLED InitializeFlipper(application); #endif [self moveMessagesToDatabase]; RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"Comm" initialProperties:nil]; if (@available(iOS 13.0, *)) { rootView.backgroundColor = [UIColor systemBackgroundColor]; } else { rootView.backgroundColor = [UIColor whiteColor]; } self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; [super application:application didFinishLaunchingWithOptions:launchOptions]; // This prevents a very small flicker from occurring before expo-splash-screen // is able to display UIView *launchScreenView = [[UIStoryboard storyboardWithName:@"SplashScreen" bundle:nil] instantiateInitialViewController] .view; launchScreenView.frame = self.window.bounds; rootView.loadingView = launchScreenView; rootView.loadingViewFadeDelay = 0; rootView.loadingViewFadeDuration = 0.001; return YES; } - (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge { // If you'd like to export some custom RCTBridgeModules that are not Expo // modules, add them here! return @[]; } - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { [RNNotifications didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [RNNotifications didFailToRegisterForRemoteNotificationsWithError:error]; } // Required for the notification event. You must call the completion handler // after handling the remote notification. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification fetchCompletionHandler: (void (^)(UIBackgroundFetchResult))completionHandler { BOOL handled = NO; if (notification[@"aps"][@"content-available"] && notification[backgroundNotificationTypeKey]) { handled = [self handleBackgroundNotification:notification fetchCompletionHandler:completionHandler]; } if (handled) { return; } [RNNotifications didReceiveRemoteNotification:notification fetchCompletionHandler:completionHandler]; } - (BOOL)handleBackgroundNotification:(NSDictionary *)notification fetchCompletionHandler: (void (^)(UIBackgroundFetchResult))completionHandler { if ([notification[backgroundNotificationTypeKey] isEqualToString:@"PING"]) { comm::GlobalNetworkSingleton::instance.scheduleOrRun( [=](comm::NetworkModule &networkModule) { networkModule.sendPong(); dispatch_async(dispatch_get_main_queue(), ^{ completionHandler(UIBackgroundFetchResultNewData); }); }); return YES; } else if ([notification[backgroundNotificationTypeKey] isEqualToString:@"CLEAR"]) { if (notification[setUnreadStatusKey] && notification[@"threadID"]) { - std::string threadID = - std::string([notification[@"threadID"] UTF8String]); + NSString *objcThreadID = notification[@"threadID"]; + std::string threadID = std::string([objcThreadID UTF8String]); // this callback may be called from inactive state so we need // to initialize the database [self attemptDatabaseInitialization]; - comm::ThreadOperations::updateSQLiteUnreadStatus(threadID, false); + [self attemptDatabaseThreadInitialization]; + self.globalDatabaseThread->scheduleTask([threadID]() mutable { + comm::ThreadOperations::updateSQLiteUnreadStatus(threadID, false); + }); } [[UNUserNotificationCenter currentNotificationCenter] getDeliveredNotificationsWithCompletionHandler:^( NSArray *notifications) { for (UNNotification *notif in notifications) { if ([notification[@"notificationId"] isEqual:notif.request.content.userInfo[@"id"]]) { NSArray *identifiers = [NSArray arrayWithObjects:notif.request.identifier, nil]; [[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:identifiers]; } } dispatch_async(dispatch_get_main_queue(), ^{ completionHandler(UIBackgroundFetchResultNewData); }); }]; return YES; } return NO; } // Required for the localNotification event. - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { [RNNotifications didReceiveLocalNotification:notification]; } - (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window { return [Orientation getOrientation]; } - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif } using JSExecutorFactory = facebook::react::JSExecutorFactory; using HermesExecutorFactory = facebook::react::HermesExecutorFactory; using Runtime = facebook::jsi::Runtime; - (std::unique_ptr)jsExecutorFactoryForBridge: (RCTBridge *)bridge { __weak __typeof(self) weakSelf = self; const auto commRuntimeInstaller = [weakSelf, bridge](facebook::jsi::Runtime &rt) { if (!bridge) { return; } __typeof(self) strongSelf = weakSelf; if (strongSelf) { std::shared_ptr nativeModule = - std::make_shared(bridge.jsCallInvoker); + std::make_shared( + bridge.jsCallInvoker, strongSelf.globalDatabaseThread); rt.global().setProperty( rt, facebook::jsi::PropNameID::forAscii(rt, "CommCoreModule"), facebook::jsi::Object::createFromHostObject(rt, nativeModule)); } }; const auto installer = reanimated::REAJSIExecutorRuntimeInstaller(bridge, commRuntimeInstaller); return std::make_unique( facebook::react::RCTJSIExecutorRuntimeInstaller(installer), JSIExecutor::defaultTimeoutInvoker, makeRuntimeConfig(3072)); } - (void)attemptDatabaseInitialization { std::string sqliteFilePath = std::string([[Tools getSQLiteFilePath] UTF8String]); // Previous Comm versions used app group location for SQLite // database, so that NotificationService was able to acces it directly. // Unfortunately it caused errores related to system locks. The code // below re-migrates SQLite from app group to app specific location // on devices where previous Comm version was installed. NSString *appGroupSQLiteFilePath = [Tools getAppGroupSQLiteFilePath]; if ([NSFileManager.defaultManager fileExistsAtPath:appGroupSQLiteFilePath] && std::rename( std::string([appGroupSQLiteFilePath UTF8String]).c_str(), sqliteFilePath.c_str())) { throw std::runtime_error( "Failed to move SQLite database from app group to default location"); } comm::SQLiteQueryExecutor::initialize(sqliteFilePath); } +- (void)attemptDatabaseThreadInitialization { + if (self.globalDatabaseThread) { + return; + } + self.globalDatabaseThread = std::make_shared("database"); +} + - (void)moveMessagesToDatabase { TemporaryMessageStorage *temporaryStorage = [[TemporaryMessageStorage alloc] init]; NSArray *messages = [temporaryStorage readAndClearMessages]; for (NSString *message in messages) { - std::string messageInfos = std::string([message UTF8String]); - comm::MessageOperationsUtilities::storeMessageInfos(messageInfos); + std::shared_ptr messageInfos = + std::make_shared(std::string([message UTF8String])); + self.globalDatabaseThread->scheduleTask([messageInfos]() { + comm::MessageOperationsUtilities::storeMessageInfos(*messageInfos); + }); } } // Copied from // ReactAndroid/src/main/java/com/facebook/hermes/reactexecutor/OnLoad.cpp static ::hermes::vm::RuntimeConfig makeRuntimeConfig(::hermes::vm::gcheapsize_t heapSizeMB) { namespace vm = ::hermes::vm; auto gcConfigBuilder = vm::GCConfig::Builder() .withName("RN") // For the next two arguments: avoid GC before TTI by initializing the // runtime to allocate directly in the old generation, but revert to // normal operation when we reach the (first) TTI point. .withAllocInYoung(false) .withRevertToYGAtTTI(true); if (heapSizeMB > 0) { gcConfigBuilder.withMaxHeapSize(heapSizeMB << 20); } return vm::RuntimeConfig::Builder() .withGCConfig(gcConfigBuilder.build()) .build(); } @end diff --git a/native/ios/Comm/CommSecureStoreIOSWrapper.h b/native/ios/Comm/CommSecureStoreIOSWrapper.h index ef7b40166..9406e5b9e 100644 --- a/native/ios/Comm/CommSecureStoreIOSWrapper.h +++ b/native/ios/Comm/CommSecureStoreIOSWrapper.h @@ -1,12 +1,13 @@ #pragma once #import #import @interface CommSecureStoreIOSWrapper : NSObject + (id)sharedInstance; - (void)set:(NSString *)key value:(NSString *)value; - (NSString *)get:(NSString *)key; +- (void)migrateOptionsForKey:(NSString *)key withVersion:(NSString *)version; @end diff --git a/native/ios/Comm/CommSecureStoreIOSWrapper.mm b/native/ios/Comm/CommSecureStoreIOSWrapper.mm index c309fd23a..c8a5f8ce4 100644 --- a/native/ios/Comm/CommSecureStoreIOSWrapper.mm +++ b/native/ios/Comm/CommSecureStoreIOSWrapper.mm @@ -1,67 +1,99 @@ #import "CommSecureStoreIOSWrapper.h" #import "CommSecureStoreIOSWrapper.h" #import @interface CommSecureStoreIOSWrapper () @property(nonatomic, strong) EXSecureStore *secureStore; @property(nonatomic, strong) NSDictionary *options; @end @interface EXSecureStore (CommEXSecureStore) - (BOOL)_setValue:(NSString *)value withKey:(NSString *)key withOptions:(NSDictionary *)options error:(NSError **)error; - (NSString *)_getValueWithKey:(NSString *)key withOptions:(NSDictionary *)options error:(NSError **)error; +- (void)_deleteValueWithKey:(NSString *)key withOptions:(NSDictionary *)options; @end @implementation CommSecureStoreIOSWrapper #pragma mark Singleton Methods + (id)sharedInstance { static CommSecureStoreIOSWrapper *shared = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ shared = [[self alloc] init]; EXModuleRegistryProvider *moduleRegistryProvider = [[EXModuleRegistryProvider alloc] init]; EXSecureStore *secureStore = (EXSecureStore *)[[moduleRegistryProvider moduleRegistry] getExportedModuleOfClass:EXSecureStore.class]; shared.secureStore = secureStore; + shared.options = + @{@"keychainAccessible" : @(EXSecureStoreAccessibleAfterFirstUnlock)}; }); return shared; } - (void)set:(NSString *)key value:(NSString *)value { if ([self secureStore] == nil) { [NSException raise:@"secure store error" format:@"secure store has not been initialized"]; } NSError *error; [[self secureStore] _setValue:value withKey:key withOptions:[self options] error:&error]; if (error != nil) { [NSException raise:@"secure store error" format:@"error occured when setting data"]; } } - (NSString *)get:(NSString *)key { if ([self secureStore] == nil) { [NSException raise:@"secure store error" format:@"secure store has not been initialized"]; } NSError *error; return [[self secureStore] _getValueWithKey:key withOptions:[self options] error:&error]; } +- (void)migrateOptionsForKey:(NSString *)key withVersion:(NSString *)version { + NSString *secureStoreKeyVersionID = [key stringByAppendingString:@".version"]; + NSString *failureProtectionCopyKey = [key stringByAppendingString:@".copy"]; + + NSString *secureStoreKeyVersion = [self get:secureStoreKeyVersionID]; + if (secureStoreKeyVersion && + [secureStoreKeyVersion isEqualToString:version]) { + return; + } + + NSString *value = [self get:key]; + NSString *valueCopy = [self get:failureProtectionCopyKey]; + + if (value) { + [self set:failureProtectionCopyKey value:value]; + [[self secureStore] _deleteValueWithKey:key withOptions:[self options]]; + } else if (valueCopy) { + value = valueCopy; + } else { + [self set:secureStoreKeyVersionID value:version]; + return; + } + + [self set:key value:value]; + [self set:secureStoreKeyVersionID value:version]; + [[self secureStore] _deleteValueWithKey:failureProtectionCopyKey + withOptions:[self options]]; +} + @end diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js index b14ada5ff..4e997c438 100644 --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -1,493 +1,529 @@ // @flow +import { isEqual } from 'lodash/lang'; import { AppState as NativeAppState, Platform, Alert } from 'react-native'; import ExitApp from 'react-native-exit-app'; import Orientation from 'react-native-orientation-locker'; import { createStore, applyMiddleware, type Store, compose } from 'redux'; import { persistStore, persistReducer } from 'redux-persist'; import thunk from 'redux-thunk'; import { setDeviceTokenActionTypes } from 'lib/actions/device-actions'; import { logOutActionTypes, deleteAccountActionTypes, logInActionTypes, sqliteOpFailure, } from 'lib/actions/user-actions'; import baseReducer from 'lib/reducers/master-reducer'; import { processThreadStoreOperations } from 'lib/reducers/thread-reducer'; import { invalidSessionDowngrade, invalidSessionRecovery, } from 'lib/shared/account-utils'; +import { isStaff } from 'lib/shared/user-utils'; import { defaultEnabledApps } from 'lib/types/enabled-apps'; import { defaultCalendarFilters } from 'lib/types/filter-types'; import type { Dispatch, BaseAction } from 'lib/types/redux-types'; import type { SetSessionPayload } from 'lib/types/session-types'; import { defaultConnectionInfo, incrementalStateSyncActionType, } from 'lib/types/socket-types'; import type { ThreadStoreOperation } from 'lib/types/thread-types'; import { updateTypes } from 'lib/types/update-types'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger'; import { setNewSessionActionType } from 'lib/utils/action-utils'; -import { convertMessageStoreOperationsToClientDBOperations } from 'lib/utils/message-ops-utils'; +import { + convertMessageStoreOperationsToClientDBOperations, + translateClientDBMessageInfosToRawMessageInfos, +} from 'lib/utils/message-ops-utils'; import { convertThreadStoreOperationsToClientDBOperations } from 'lib/utils/thread-ops-utils'; import { defaultNavInfo } from '../navigation/default-state'; import { getGlobalNavContext } from '../navigation/icky-global'; import { activeMessageListSelector } from '../navigation/nav-selectors'; import { defaultNotifPermissionAlertInfo } from '../push/alerts'; import { reduceThreadIDsToNotifIDs } from '../push/reducer'; import reactotron from '../reactotron'; import { defaultDeviceCameraInfo } from '../types/camera'; import { defaultConnectivityInfo } from '../types/connectivity'; import { defaultGlobalThemeInfo } from '../types/themes'; import { defaultURLPrefix, natNodeServer, setCustomServer, getDevServerHostname, } from '../utils/url-utils'; import { resetUserStateActionType, recordNotifPermissionAlertActionType, recordAndroidNotificationActionType, clearAndroidNotificationsActionType, rescindAndroidNotificationActionType, updateDimensionsActiveType, updateConnectivityActiveType, updateThemeInfoActionType, updateDeviceCameraInfoActionType, updateDeviceOrientationActionType, updateThreadLastNavigatedActionType, backgroundActionTypes, setReduxStateActionType, type Action, } from './action-types'; import { remoteReduxDevServerConfig } from './dev-tools'; import { defaultDimensionsInfo } from './dimensions-updater.react'; import { persistConfig, setPersistor } from './persist'; import type { AppState } from './state-types'; const defaultState = ({ navInfo: defaultNavInfo, currentUserInfo: null, entryStore: { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }, threadStore: { threadInfos: {}, }, userStore: { userInfos: {}, inconsistencyReports: [], }, messageStore: { messages: {}, threads: {}, local: {}, currentAsOf: 0, }, updatesCurrentAsOf: 0, loadingStatuses: {}, calendarFilters: defaultCalendarFilters, cookie: null, deviceToken: null, dataLoaded: false, urlPrefix: defaultURLPrefix, customServer: natNodeServer, threadIDsToNotifIDs: {}, notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, connection: defaultConnectionInfo(Platform.OS), watchedThreadIDs: [], lifecycleState: 'active', enabledApps: defaultEnabledApps, reportStore: { enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, queuedReports: [], }, nextLocalID: 0, _persist: null, dimensions: defaultDimensionsInfo, connectivity: defaultConnectivityInfo, globalThemeInfo: defaultGlobalThemeInfo, deviceCameraInfo: defaultDeviceCameraInfo, deviceOrientation: Orientation.getInitialOrientation(), frozen: false, }: AppState); function reducer(state: AppState = defaultState, action: Action) { if (action.type === setReduxStateActionType) { return action.payload.state; } if ( (action.type === setNewSessionActionType && invalidSessionDowngrade( state, action.payload.sessionChange.currentUserInfo, action.payload.preRequestUserState, )) || (action.type === logOutActionTypes.success && invalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, )) || (action.type === deleteAccountActionTypes.success && invalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, )) ) { return state; } if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.sessionChange.currentUserInfo, action.payload.source, )) || (action.type === logInActionTypes.success && invalidSessionRecovery( state, action.payload.currentUserInfo, action.payload.source, )) ) { return state; } const threadIDsToNotifIDs = reduceThreadIDsToNotifIDs( state.threadIDsToNotifIDs, action, ); state = { ...state, threadIDsToNotifIDs }; if ( action.type === recordAndroidNotificationActionType || action.type === clearAndroidNotificationsActionType || action.type === rescindAndroidNotificationActionType ) { return state; } if (action.type === setCustomServer) { return { ...state, customServer: action.payload, }; } else if (action.type === recordNotifPermissionAlertActionType) { return { ...state, notifPermissionAlertInfo: { totalAlerts: state.notifPermissionAlertInfo.totalAlerts + 1, lastAlertTime: action.payload.time, }, }; } else if (action.type === resetUserStateActionType) { const cookie = state.cookie && state.cookie.startsWith('anonymous=') ? state.cookie : null; const currentUserInfo = state.currentUserInfo && state.currentUserInfo.anonymous ? state.currentUserInfo : null; return { ...state, currentUserInfo, cookie, }; } else if (action.type === updateDimensionsActiveType) { return { ...state, dimensions: { ...state.dimensions, ...action.payload, }, }; } else if (action.type === updateConnectivityActiveType) { return { ...state, connectivity: action.payload, }; } else if (action.type === updateThemeInfoActionType) { return { ...state, globalThemeInfo: { ...state.globalThemeInfo, ...action.payload, }, }; } else if (action.type === updateDeviceCameraInfoActionType) { return { ...state, deviceCameraInfo: { ...state.deviceCameraInfo, ...action.payload, }, }; } else if (action.type === updateDeviceOrientationActionType) { return { ...state, deviceOrientation: action.payload, }; } else if (action.type === setDeviceTokenActionTypes.success) { return { ...state, deviceToken: action.payload, }; } else if (action.type === updateThreadLastNavigatedActionType) { const { threadID, time } = action.payload; if (state.messageStore.threads[threadID]) { state = { ...state, messageStore: { ...state.messageStore, threads: { ...state.messageStore.threads, [threadID]: { ...state.messageStore.threads[threadID], lastNavigatedTo: time, }, }, }, }; } return state; } if (action.type === setNewSessionActionType) { sessionInvalidationAlert(action.payload); state = { ...state, cookie: action.payload.sessionChange.cookie, }; } else if (action.type === incrementalStateSyncActionType) { let wipeDeviceToken = false; for (const update of action.payload.updatesResult.newUpdates) { if ( update.type === updateTypes.BAD_DEVICE_TOKEN && update.deviceToken === state.deviceToken ) { wipeDeviceToken = true; break; } } if (wipeDeviceToken) { state = { ...state, deviceToken: null, }; } } const baseReducerResult = baseReducer(state, (action: BaseAction)); state = baseReducerResult.state; const { storeOperations } = baseReducerResult; const { threadStoreOperations, messageStoreOperations } = storeOperations; const fixUnreadActiveThreadResult = fixUnreadActiveThread(state, action); state = fixUnreadActiveThreadResult.state; const threadStoreOperationsWithUnreadFix = [ ...threadStoreOperations, ...fixUnreadActiveThreadResult.threadStoreOperations, ]; const convertedThreadStoreOperations = convertThreadStoreOperationsToClientDBOperations( threadStoreOperationsWithUnreadFix, ); const convertedMessageStoreOperations = convertMessageStoreOperationsToClientDBOperations( messageStoreOperations, ); + + if (convertedMessageStoreOperations.length > 0) { + global.CommCoreModule.processMessageStoreOperationsSync( + convertedMessageStoreOperations, + ); + } + + const crashReportsEnabled = state.reportStore.enabledReports.crashReports; + const viewerID = state.currentUserInfo?.id; + + try { + const messages = global.CommCoreModule.getAllMessagesSync(); + const rawMsgsFromSQLite = translateClientDBMessageInfosToRawMessageInfos( + messages, + ); + + const ignoreList = [ + '@@INIT', + 'persist/REHYDRATE', + 'persist/PERSIST', + 'SET_THREAD_STORE', + ]; + if ( + !isEqual(rawMsgsFromSQLite, state.messageStore.messages) && + !ignoreList.includes(action.type) && + !action.type.includes('@@redux/INIT') + ) { + Alert.alert(`${action.type}: NOT EQUAL`); + } + } catch (e) { + if ((__DEV__ || (viewerID && isStaff(viewerID))) && crashReportsEnabled) { + throw e; + } + console.log(e.message); + } + (async () => { try { const promises = []; if (convertedThreadStoreOperations.length > 0) { promises.push( global.CommCoreModule.processThreadStoreOperations( convertedThreadStoreOperations, ), ); } - if (convertedMessageStoreOperations.length > 0) { - promises.push( - global.CommCoreModule.processMessageStoreOperations( - convertedMessageStoreOperations, - ), - ); - } await Promise.all(promises); } catch { dispatch({ type: setNewSessionActionType, payload: { sessionChange: { cookie: null, cookieInvalidated: false, currentUserInfo: state.currentUserInfo, }, preRequestUserState: { currentUserInfo: state.currentUserInfo, sessionID: undefined, cookie: state.cookie, }, error: null, source: sqliteOpFailure, }, }); await persistor.flush(); ExitApp.exitApp(); } })(); return state; } function sessionInvalidationAlert(payload: SetSessionPayload) { if ( !payload.sessionChange.cookieInvalidated || !payload.preRequestUserState || !payload.preRequestUserState.currentUserInfo || payload.preRequestUserState.currentUserInfo.anonymous ) { return; } if (payload.error === 'client_version_unsupported') { const app = Platform.select({ ios: 'App Store', android: 'Play Store', }); Alert.alert( 'App out of date', 'Your app version is pretty old, and the server doesn’t know how to ' + `speak to it anymore. Please use the ${app} app to update!`, [{ text: 'OK' }], { cancelable: true }, ); } else { Alert.alert( 'Session invalidated', 'We’re sorry, but your session was invalidated by the server. ' + 'Please log in again.', [{ text: 'OK' }], { cancelable: true }, ); } } // Makes sure a currently focused thread is never unread. Note that we consider // a backgrounded NativeAppState to actually be active if it last changed to // inactive more than 10 seconds ago. This is because there is a delay when // NativeAppState is updating in response to a foreground, and actions don't get // processed more than 10 seconds after a backgrounding anyways. However we // don't consider this for action types that can be expected to happen while the // app is backgrounded. type FixUnreadActiveThreadResult = { +state: AppState, +threadStoreOperations: $ReadOnlyArray, }; + function fixUnreadActiveThread( state: AppState, action: *, ): FixUnreadActiveThreadResult { const navContext = getGlobalNavContext(); const activeThread = activeMessageListSelector(navContext); if ( !activeThread || !state.threadStore.threadInfos[activeThread]?.currentUser.unread || (NativeAppState.currentState !== 'active' && (appLastBecameInactive + 10000 >= Date.now() || backgroundActionTypes.has(action.type))) ) { return { state, threadStoreOperations: [] }; } const updatedActiveThreadInfo = { ...state.threadStore.threadInfos[activeThread], currentUser: { ...state.threadStore.threadInfos[activeThread].currentUser, unread: false, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: activeThread, threadInfo: updatedActiveThreadInfo, }, }, ]; const updatedThreadStore = processThreadStoreOperations( state.threadStore, threadStoreOperations, ); return { state: { ...state, threadStore: updatedThreadStore }, threadStoreOperations, }; } let appLastBecameInactive = 0; + function appBecameInactive() { appLastBecameInactive = Date.now(); } const middlewares = [thunk, reduxLoggerMiddleware]; if (__DEV__) { const createDebugger = require('redux-flipper').default; middlewares.push(createDebugger()); } const middleware = applyMiddleware(...middlewares); let composeFunc = compose; if (__DEV__ && global.HermesInternal) { const { composeWithDevTools } = require('remote-redux-devtools/src'); composeFunc = composeWithDevTools({ name: 'Redux', hostname: getDevServerHostname(), ...remoteReduxDevServerConfig, }); } else if (global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeFunc = global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'Redux', }); } let enhancers; if (reactotron) { enhancers = composeFunc(middleware, reactotron.createEnhancer()); } else { enhancers = composeFunc(middleware); } const store: Store = createStore( persistReducer(persistConfig, reducer), defaultState, enhancers, ); const persistor = persistStore(store); setPersistor(persistor); const unsafeDispatch: any = store.dispatch; const dispatch: Dispatch = unsafeDispatch; export { store, dispatch, appBecameInactive };