diff --git a/lib/ops/community-store-ops.js b/lib/ops/community-store-ops.js index 1d9b0a1a7..ce567f6cb 100644 --- a/lib/ops/community-store-ops.js +++ b/lib/ops/community-store-ops.js @@ -1,113 +1,126 @@ // @flow import type { BaseStoreOpsHandlers } from './base-ops.js'; import type { CommunityInfo, CommunityInfos, CommunityStore, } from '../types/community-types.js'; // client types export type ReplaceCommunityOperation = { +type: 'replace_community', +payload: { +id: string, +communityInfo: CommunityInfo }, }; export type RemoveCommunitiesOperation = { +type: 'remove_communities', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveAllCommunitiesOperation = { +type: 'remove_all_communities', }; export type CommunityStoreOperation = | ReplaceCommunityOperation | RemoveCommunitiesOperation | RemoveAllCommunitiesOperation; // SQLite types export type ClientDBCommunityInfo = { +id: string, +communityInfo: string, }; export type ClientDBReplaceCommunityOperation = { +type: 'replace_community', +payload: ClientDBCommunityInfo, }; export type ClientDBCommunityStoreOperation = | ClientDBReplaceCommunityOperation | RemoveCommunitiesOperation | RemoveAllCommunitiesOperation; +function convertCommunityInfoToClientDBCommunityInfo({ + id, + communityInfo, +}: { + +id: string, + +communityInfo: CommunityInfo, +}): ClientDBCommunityInfo { + return { + id, + communityInfo: JSON.stringify(communityInfo), + }; +} + const communityStoreOpsHandlers: BaseStoreOpsHandlers< CommunityStore, CommunityStoreOperation, ClientDBCommunityStoreOperation, CommunityInfos, ClientDBCommunityInfo, > = { processStoreOperations( communityStore: CommunityStore, ops: $ReadOnlyArray, ): CommunityStore { if (ops.length === 0) { return communityStore; } let processedCommunityInfos = { ...communityStore.communityInfos }; for (const operation of ops) { if (operation.type === 'replace_community') { processedCommunityInfos[operation.payload.id] = operation.payload.communityInfo; } else if (operation.type === 'remove_communities') { for (const id of operation.payload.ids) { delete processedCommunityInfos[id]; } } else if (operation.type === 'remove_all_communities') { processedCommunityInfos = {}; } } return { ...communityStore, communityInfos: processedCommunityInfos }; }, convertOpsToClientDBOps( ops: $ReadOnlyArray, ): $ReadOnlyArray { return ops.map(operation => { if ( operation.type === 'remove_communities' || operation.type === 'remove_all_communities' ) { return operation; } return { type: 'replace_community', - payload: { - id: operation.payload.id, - communityInfo: JSON.stringify(operation.payload.communityInfo), - }, + payload: convertCommunityInfoToClientDBCommunityInfo(operation.payload), }; }); }, translateClientDBData( communites: $ReadOnlyArray, ): CommunityInfos { const communityInfos: { [id: string]: CommunityInfo } = {}; communites.forEach(dbCommunity => { communityInfos[dbCommunity.id] = JSON.parse(dbCommunity.communityInfo); }); return communityInfos; }, }; -export { communityStoreOpsHandlers }; +export { + communityStoreOpsHandlers, + convertCommunityInfoToClientDBCommunityInfo, +}; diff --git a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h index b602acaa5..30d907292 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h @@ -1,109 +1,114 @@ #pragma once #include "../CryptoTools/Persist.h" +#include "entities/CommunityInfo.h" #include "entities/Draft.h" #include "entities/KeyserverInfo.h" #include "entities/Message.h" #include "entities/MessageStoreThread.h" #include "entities/OlmPersistAccount.h" #include "entities/OlmPersistSession.h" #include "entities/PersistItem.h" #include "entities/Report.h" #include "entities/Thread.h" #include "entities/UserInfo.h" #include namespace comm { /** * if any initialization/cleaning up steps are required for specific * database managers they should appear in constructors/destructors * following the RAII pattern */ class DatabaseQueryExecutor { public: virtual std::string getDraft(std::string key) const = 0; virtual std::unique_ptr getThread(std::string threadID) const = 0; virtual void updateDraft(std::string key, std::string text) const = 0; virtual bool moveDraft(std::string oldKey, std::string newKey) const = 0; virtual std::vector getAllDrafts() const = 0; virtual void removeAllDrafts() const = 0; virtual void removeDrafts(const std::vector &ids) const = 0; virtual void removeAllMessages() const = 0; virtual std::vector>> getAllMessages() const = 0; virtual void removeMessages(const std::vector &ids) const = 0; virtual void removeMessagesForThreads(const std::vector &threadIDs) const = 0; virtual void replaceMessage(const Message &message) const = 0; virtual void rekeyMessage(std::string from, std::string to) const = 0; virtual void removeAllMedia() const = 0; virtual void replaceMessageStoreThreads( const std::vector &threads) const = 0; virtual void removeMessageStoreThreads(const std::vector &ids) const = 0; virtual void removeAllMessageStoreThreads() const = 0; virtual std::vector getAllMessageStoreThreads() const = 0; virtual void removeMediaForMessages(const std::vector &msg_ids) const = 0; virtual void removeMediaForMessage(std::string msg_id) const = 0; virtual void removeMediaForThreads(const std::vector &thread_ids) const = 0; virtual void replaceMedia(const Media &media) const = 0; virtual void rekeyMediaContainers(std::string from, std::string to) const = 0; virtual std::vector getAllThreads() const = 0; virtual void removeThreads(std::vector ids) const = 0; virtual void replaceThread(const Thread &thread) const = 0; virtual void removeAllThreads() const = 0; virtual void replaceReport(const Report &report) const = 0; virtual void removeReports(const std::vector &ids) const = 0; virtual void removeAllReports() const = 0; virtual std::vector getAllReports() const = 0; virtual void setPersistStorageItem(std::string key, std::string item) const = 0; virtual void removePersistStorageItem(std::string key) const = 0; virtual std::string getPersistStorageItem(std::string key) const = 0; virtual void replaceUser(const UserInfo &user_info) const = 0; virtual void removeUsers(const std::vector &ids) const = 0; virtual void removeAllUsers() const = 0; virtual std::vector getAllUsers() const = 0; virtual void replaceKeyserver(const KeyserverInfo &keyserver_info) const = 0; virtual void removeKeyservers(const std::vector &ids) const = 0; virtual void removeAllKeyservers() const = 0; virtual std::vector getAllKeyservers() const = 0; + virtual void replaceCommunity(const CommunityInfo &community_info) const = 0; + virtual void removeCommunities(const std::vector &ids) const = 0; + virtual void removeAllCommunities() const = 0; + virtual std::vector getAllCommunities() const = 0; virtual void beginTransaction() const = 0; virtual void commitTransaction() const = 0; virtual void rollbackTransaction() const = 0; virtual std::vector getOlmPersistSessionsData() const = 0; virtual std::optional getOlmPersistAccountData() const = 0; virtual void storeOlmPersistSession(const OlmPersistSession &session) const = 0; virtual void storeOlmPersistAccount(const std::string &accountData) const = 0; virtual void storeOlmPersistData(crypto::Persist persist) const = 0; virtual void setNotifyToken(std::string token) const = 0; virtual void clearNotifyToken() const = 0; virtual void setCurrentUserID(std::string userID) const = 0; virtual std::string getCurrentUserID() const = 0; virtual void setMetadata(std::string entry_name, std::string data) const = 0; virtual void clearMetadata(std::string entry_name) const = 0; virtual std::string getMetadata(std::string entry_name) const = 0; virtual void restoreFromMainCompaction( std::string mainCompactionPath, std::string mainCompactionEncryptionKey) const = 0; virtual void restoreFromBackupLog(const std::vector &backupLog) const = 0; #ifdef EMSCRIPTEN virtual std::vector getAllThreadsWeb() const = 0; virtual void replaceThreadWeb(const WebThread &thread) const = 0; virtual std::vector getAllMessagesWeb() const = 0; virtual void replaceMessageWeb(const WebMessage &message) const = 0; virtual NullableString getOlmPersistAccountDataWeb() const = 0; #else virtual void createMainCompaction(std::string backupID) const = 0; virtual void captureBackupLogs() const = 0; #endif }; } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp index 184423186..a6a0b2539 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp @@ -1,1938 +1,1981 @@ #include "SQLiteQueryExecutor.h" #include "Logger.h" +#include "entities/CommunityInfo.h" #include "entities/EntityQueryHelpers.h" #include "entities/KeyserverInfo.h" #include "entities/Metadata.h" #include "entities/UserInfo.h" #include #include #include #ifndef EMSCRIPTEN #include "CommSecureStore.h" #include "PlatformSpecificTools.h" #include "StaffUtils.h" #endif #define ACCOUNT_ID 1 namespace comm { std::string SQLiteQueryExecutor::sqliteFilePath; std::string SQLiteQueryExecutor::encryptionKey; std::once_flag SQLiteQueryExecutor::initialized; int SQLiteQueryExecutor::sqlcipherEncryptionKeySize = 64; // Should match constant defined in `native_rust_library/src/constants.rs` std::string SQLiteQueryExecutor::secureStoreEncryptionKeyID = "comm.encryptionKey"; int SQLiteQueryExecutor::backupLogsEncryptionKeySize = 32; std::string SQLiteQueryExecutor::secureStoreBackupLogsEncryptionKeyID = "comm.backupLogsEncryptionKey"; std::string SQLiteQueryExecutor::backupLogsEncryptionKey; #ifndef EMSCRIPTEN NativeSQLiteConnectionManager SQLiteQueryExecutor::connectionManager; #else SQLiteConnectionManager SQLiteQueryExecutor::connectionManager; #endif bool create_table(sqlite3 *db, std::string query, std::string tableName) { char *error; sqlite3_exec(db, query.c_str(), nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error creating '" << tableName << "' table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_drafts_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS drafts (threadID TEXT UNIQUE PRIMARY KEY, " "text TEXT);"; return create_table(db, query, "drafts"); } bool rename_threadID_to_key(sqlite3 *db) { sqlite3_stmt *key_column_stmt; sqlite3_prepare_v2( db, "SELECT name AS col_name FROM pragma_table_xinfo ('drafts') WHERE " "col_name='key';", -1, &key_column_stmt, nullptr); sqlite3_step(key_column_stmt); auto num_bytes = sqlite3_column_bytes(key_column_stmt, 0); sqlite3_finalize(key_column_stmt); if (num_bytes) { return true; } char *error; sqlite3_exec( db, "ALTER TABLE drafts RENAME COLUMN `threadID` TO `key`;", nullptr, nullptr, &error); if (error) { std::ostringstream stringStream; stringStream << "Error occurred renaming threadID column in drafts table " << "to key: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } return true; } bool create_persist_account_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS olm_persist_account(" "id INTEGER UNIQUE PRIMARY KEY NOT NULL, " "account_data TEXT NOT NULL);"; return create_table(db, query, "olm_persist_account"); } bool create_persist_sessions_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS olm_persist_sessions(" "target_user_id TEXT UNIQUE PRIMARY KEY NOT NULL, " "session_data TEXT NOT NULL);"; return create_table(db, query, "olm_persist_sessions"); } bool drop_messages_table(sqlite3 *db) { char *error; sqlite3_exec(db, "DROP TABLE IF EXISTS messages;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error dropping 'messages' table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool recreate_messages_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS messages ( " "id TEXT UNIQUE PRIMARY KEY NOT NULL, " "local_id TEXT, " "thread TEXT NOT NULL, " "user TEXT NOT NULL, " "type INTEGER NOT NULL, " "future_type INTEGER, " "content TEXT, " "time INTEGER NOT NULL);"; return create_table(db, query, "messages"); } bool create_messages_idx_thread_time(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE INDEX IF NOT EXISTS messages_idx_thread_time " "ON messages (thread, time);", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error creating (thread, time) index on messages table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_media_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS media ( " "id TEXT UNIQUE PRIMARY KEY NOT NULL, " "container TEXT NOT NULL, " "thread TEXT NOT NULL, " "uri TEXT NOT NULL, " "type TEXT NOT NULL, " "extras TEXT NOT NULL);"; return create_table(db, query, "media"); } bool create_media_idx_container(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE INDEX IF NOT EXISTS media_idx_container " "ON media (container);", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error creating (container) index on media table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_threads_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS threads ( " "id TEXT UNIQUE PRIMARY KEY NOT NULL, " "type INTEGER NOT NULL, " "name TEXT, " "description TEXT, " "color TEXT NOT NULL, " "creation_time BIGINT NOT NULL, " "parent_thread_id TEXT, " "containing_thread_id TEXT, " "community TEXT, " "members TEXT NOT NULL, " "roles TEXT NOT NULL, " "current_user TEXT NOT NULL, " "source_message_id TEXT, " "replies_count INTEGER NOT NULL);"; return create_table(db, query, "threads"); } bool update_threadID_for_pending_threads_in_drafts(sqlite3 *db) { char *error; sqlite3_exec( db, "UPDATE drafts SET key = " "REPLACE(REPLACE(REPLACE(REPLACE(key, 'type4/', '')," "'type5/', ''),'type6/', ''),'type7/', '')" "WHERE key LIKE 'pending/%'", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error update pending threadIDs on drafts table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool enable_write_ahead_logging_mode(sqlite3 *db) { char *error; sqlite3_exec(db, "PRAGMA journal_mode=wal;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error enabling write-ahead logging mode: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_metadata_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS metadata ( " "name TEXT UNIQUE PRIMARY KEY NOT NULL, " "data TEXT);"; return create_table(db, query, "metadata"); } bool add_not_null_constraint_to_drafts(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE TABLE IF NOT EXISTS temporary_drafts (" "key TEXT UNIQUE PRIMARY KEY NOT NULL, " "text TEXT NOT NULL);" "INSERT INTO temporary_drafts SELECT * FROM drafts " "WHERE key IS NOT NULL AND text IS NOT NULL;" "DROP TABLE drafts;" "ALTER TABLE temporary_drafts RENAME TO drafts;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error adding NOT NULL constraint to drafts table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool add_not_null_constraint_to_metadata(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE TABLE IF NOT EXISTS temporary_metadata (" "name TEXT UNIQUE PRIMARY KEY NOT NULL, " "data TEXT NOT NULL);" "INSERT INTO temporary_metadata SELECT * FROM metadata " "WHERE data IS NOT NULL;" "DROP TABLE metadata;" "ALTER TABLE temporary_metadata RENAME TO metadata;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error adding NOT NULL constraint to metadata table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool add_avatar_column_to_threads_table(sqlite3 *db) { char *error; sqlite3_exec( db, "ALTER TABLE threads ADD COLUMN avatar TEXT;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error adding avatar column to threads table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool add_pinned_count_column_to_threads(sqlite3 *db) { sqlite3_stmt *pinned_column_stmt; sqlite3_prepare_v2( db, "SELECT name AS col_name FROM pragma_table_xinfo ('threads') WHERE " "col_name='pinned_count';", -1, &pinned_column_stmt, nullptr); sqlite3_step(pinned_column_stmt); auto num_bytes = sqlite3_column_bytes(pinned_column_stmt, 0); sqlite3_finalize(pinned_column_stmt); if (num_bytes) { return true; } char *error; sqlite3_exec( db, "ALTER TABLE threads ADD COLUMN pinned_count INTEGER NOT NULL DEFAULT 0;", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error adding pinned_count column to threads table: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } bool create_message_store_threads_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS message_store_threads (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " start_reached INTEGER NOT NULL," " last_navigated_to BIGINT NOT NULL," " last_pruned BIGINT NOT NULL" ");"; return create_table(db, query, "message_store_threads"); } bool create_reports_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS reports (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " report TEXT NOT NULL" ");"; return create_table(db, query, "reports"); } bool create_persist_storage_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS persist_storage (" " key TEXT UNIQUE PRIMARY KEY NOT NULL," " item TEXT NOT NULL" ");"; return create_table(db, query, "persist_storage"); } bool recreate_message_store_threads_table(sqlite3 *db) { char *errMsg = 0; // 1. Create table without `last_navigated_to` or `last_pruned`. std::string create_new_table_query = "CREATE TABLE IF NOT EXISTS temp_message_store_threads (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " start_reached INTEGER NOT NULL" ");"; if (sqlite3_exec(db, create_new_table_query.c_str(), NULL, NULL, &errMsg) != SQLITE_OK) { Logger::log( "Error creating temp_message_store_threads: " + std::string{errMsg}); sqlite3_free(errMsg); return false; } // 2. Dump data from existing `message_store_threads` table into temp table. std::string copy_data_query = "INSERT INTO temp_message_store_threads (id, start_reached)" "SELECT id, start_reached FROM message_store_threads;"; if (sqlite3_exec(db, copy_data_query.c_str(), NULL, NULL, &errMsg) != SQLITE_OK) { Logger::log( "Error dumping data from existing message_store_threads to " "temp_message_store_threads: " + std::string{errMsg}); sqlite3_free(errMsg); return false; } // 3. Drop the existing `message_store_threads` table. std::string drop_old_table_query = "DROP TABLE message_store_threads;"; if (sqlite3_exec(db, drop_old_table_query.c_str(), NULL, NULL, &errMsg) != SQLITE_OK) { Logger::log( "Error dropping message_store_threads table: " + std::string{errMsg}); sqlite3_free(errMsg); return false; } // 4. Rename the temp table back to `message_store_threads`. std::string rename_table_query = "ALTER TABLE temp_message_store_threads RENAME TO message_store_threads;"; if (sqlite3_exec(db, rename_table_query.c_str(), NULL, NULL, &errMsg) != SQLITE_OK) { Logger::log( "Error renaming temp_message_store_threads to message_store_threads: " + std::string{errMsg}); sqlite3_free(errMsg); return false; } return true; } bool create_users_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS users (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " user_info TEXT NOT NULL" ");"; return create_table(db, query, "users"); } bool create_keyservers_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS keyservers (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " keyserver_info TEXT NOT NULL" ");"; return create_table(db, query, "keyservers"); } bool enable_rollback_journal_mode(sqlite3 *db) { char *error; sqlite3_exec(db, "PRAGMA journal_mode=DELETE;", nullptr, nullptr, &error); if (!error) { return true; } std::stringstream error_message; error_message << "Error disabling write-ahead logging mode: " << error; Logger::log(error_message.str()); sqlite3_free(error); return false; } bool create_communities_table(sqlite3 *db) { std::string query = "CREATE TABLE IF NOT EXISTS communities (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " community_info TEXT NOT NULL" ");"; return create_table(db, query, "communities"); } bool create_schema(sqlite3 *db) { char *error; sqlite3_exec( db, "CREATE TABLE IF NOT EXISTS drafts (" " key TEXT UNIQUE PRIMARY KEY NOT NULL," " text TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS messages (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " local_id TEXT," " thread TEXT NOT NULL," " user TEXT NOT NULL," " type INTEGER NOT NULL," " future_type INTEGER," " content TEXT," " time INTEGER NOT NULL" ");" "CREATE TABLE IF NOT EXISTS olm_persist_account (" " id INTEGER UNIQUE PRIMARY KEY NOT NULL," " account_data TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS olm_persist_sessions (" " target_user_id TEXT UNIQUE PRIMARY KEY NOT NULL," " session_data TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS media (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " container TEXT NOT NULL," " thread TEXT NOT NULL," " uri TEXT NOT NULL," " type TEXT NOT NULL," " extras TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS threads (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " type INTEGER NOT NULL," " name TEXT," " description TEXT," " color TEXT NOT NULL," " creation_time BIGINT NOT NULL," " parent_thread_id TEXT," " containing_thread_id TEXT," " community TEXT," " members TEXT NOT NULL," " roles TEXT NOT NULL," " current_user TEXT NOT NULL," " source_message_id TEXT," " replies_count INTEGER NOT NULL," " avatar TEXT," " pinned_count INTEGER NOT NULL DEFAULT 0" ");" "CREATE TABLE IF NOT EXISTS metadata (" " name TEXT UNIQUE PRIMARY KEY NOT NULL," " data TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS message_store_threads (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " start_reached INTEGER NOT NULL" ");" "CREATE TABLE IF NOT EXISTS reports (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " report TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS persist_storage (" " key TEXT UNIQUE PRIMARY KEY NOT NULL," " item TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS users (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " user_info TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS keyservers (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " keyserver_info TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS communities (" " id TEXT UNIQUE PRIMARY KEY NOT NULL," " community_info TEXT NOT NULL" ");" "CREATE INDEX IF NOT EXISTS media_idx_container" " ON media (container);" "CREATE INDEX IF NOT EXISTS messages_idx_thread_time" " ON messages (thread, time);", nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream stringStream; stringStream << "Error creating tables: " << error; Logger::log(stringStream.str()); sqlite3_free(error); return false; } void set_encryption_key( sqlite3 *db, const std::string &encryptionKey = SQLiteQueryExecutor::encryptionKey) { std::string set_encryption_key_query = "PRAGMA key = \"x'" + encryptionKey + "'\";"; char *error_set_key; sqlite3_exec( db, set_encryption_key_query.c_str(), nullptr, nullptr, &error_set_key); if (error_set_key) { std::ostringstream error_message; error_message << "Failed to set encryption key: " << error_set_key; throw std::system_error( ECANCELED, std::generic_category(), error_message.str()); } } int get_database_version(sqlite3 *db) { sqlite3_stmt *user_version_stmt; sqlite3_prepare_v2( db, "PRAGMA user_version;", -1, &user_version_stmt, nullptr); sqlite3_step(user_version_stmt); int current_user_version = sqlite3_column_int(user_version_stmt, 0); sqlite3_finalize(user_version_stmt); return current_user_version; } bool set_database_version(sqlite3 *db, int db_version) { std::stringstream update_version; update_version << "PRAGMA user_version=" << db_version << ";"; auto update_version_str = update_version.str(); char *error; sqlite3_exec(db, update_version_str.c_str(), nullptr, nullptr, &error); if (!error) { return true; } std::ostringstream errorStream; errorStream << "Error setting database version to " << db_version << ": " << error; Logger::log(errorStream.str()); sqlite3_free(error); return false; } // We don't want to run `PRAGMA key = ...;` // on main web database. The context is here: // https://linear.app/comm/issue/ENG-6398/issues-with-sqlcipher-on-web void default_on_db_open_callback(sqlite3 *db) { #ifndef EMSCRIPTEN set_encryption_key(db); #endif } // This is a temporary solution. In future we want to keep // a separate table for blob hashes. Tracked on Linear: // https://linear.app/comm/issue/ENG-6261/introduce-blob-hash-table std::string blob_hash_from_blob_service_uri(const std::string &media_uri) { static const std::string blob_service_prefix = "comm-blob-service://"; return media_uri.substr(blob_service_prefix.size()); } bool file_exists(const std::string &file_path) { std::ifstream file(file_path.c_str()); return file.good(); } void attempt_delete_file( const std::string &file_path, const char *error_message) { if (std::remove(file_path.c_str())) { throw std::system_error(errno, std::generic_category(), error_message); } } void attempt_rename_file( const std::string &old_path, const std::string &new_path, const char *error_message) { if (std::rename(old_path.c_str(), new_path.c_str())) { throw std::system_error(errno, std::generic_category(), error_message); } } bool is_database_queryable( sqlite3 *db, bool use_encryption_key, const std::string &path = SQLiteQueryExecutor::sqliteFilePath, const std::string &encryptionKey = SQLiteQueryExecutor::encryptionKey) { char *err_msg; sqlite3_open(path.c_str(), &db); // According to SQLCipher documentation running some SELECT is the only way to // check for key validity if (use_encryption_key) { set_encryption_key(db, encryptionKey); } sqlite3_exec( db, "SELECT COUNT(*) FROM sqlite_master;", nullptr, nullptr, &err_msg); sqlite3_close(db); return !err_msg; } void validate_encryption() { std::string temp_encrypted_db_path = SQLiteQueryExecutor::sqliteFilePath + "_temp_encrypted"; bool temp_encrypted_exists = file_exists(temp_encrypted_db_path); bool default_location_exists = file_exists(SQLiteQueryExecutor::sqliteFilePath); if (temp_encrypted_exists && default_location_exists) { Logger::log( "Previous encryption attempt failed. Repeating encryption process from " "the beginning."); attempt_delete_file( temp_encrypted_db_path, "Failed to delete corrupted encrypted database."); } else if (temp_encrypted_exists && !default_location_exists) { Logger::log( "Moving temporary encrypted database to default location failed in " "previous encryption attempt. Repeating rename step."); attempt_rename_file( temp_encrypted_db_path, SQLiteQueryExecutor::sqliteFilePath, "Failed to move encrypted database to default location."); return; } else if (!default_location_exists) { Logger::log( "Database not present yet. It will be created encrypted under default " "path."); return; } sqlite3 *db; if (is_database_queryable(db, true)) { Logger::log( "Database exists under default path and it is correctly encrypted."); return; } if (!is_database_queryable(db, false)) { Logger::log( "Database exists but it is encrypted with key that was lost. " "Attempting database deletion. New encrypted one will be created."); attempt_delete_file( SQLiteQueryExecutor::sqliteFilePath.c_str(), "Failed to delete database encrypted with lost key."); return; } else { Logger::log( "Database exists but it is not encrypted. Attempting encryption " "process."); } sqlite3_open(SQLiteQueryExecutor::sqliteFilePath.c_str(), &db); std::string createEncryptedCopySQL = "ATTACH DATABASE '" + temp_encrypted_db_path + "' AS encrypted_comm " "KEY \"x'" + SQLiteQueryExecutor::encryptionKey + "'\";" "SELECT sqlcipher_export('encrypted_comm');" "DETACH DATABASE encrypted_comm;"; char *encryption_error; sqlite3_exec( db, createEncryptedCopySQL.c_str(), nullptr, nullptr, &encryption_error); if (encryption_error) { throw std::system_error( ECANCELED, std::generic_category(), "Failed to create encrypted copy of the original database."); } sqlite3_close(db); attempt_delete_file( SQLiteQueryExecutor::sqliteFilePath, "Failed to delete unencrypted database."); attempt_rename_file( temp_encrypted_db_path, SQLiteQueryExecutor::sqliteFilePath, "Failed to move encrypted database to default location."); Logger::log("Encryption completed successfully."); } typedef bool ShouldBeInTransaction; typedef std::function MigrateFunction; typedef std::pair SQLiteMigration; std::vector> migrations{ {{1, {create_drafts_table, true}}, {2, {rename_threadID_to_key, true}}, {4, {create_persist_account_table, true}}, {5, {create_persist_sessions_table, true}}, {15, {create_media_table, true}}, {16, {drop_messages_table, true}}, {17, {recreate_messages_table, true}}, {18, {create_messages_idx_thread_time, true}}, {19, {create_media_idx_container, true}}, {20, {create_threads_table, true}}, {21, {update_threadID_for_pending_threads_in_drafts, true}}, {22, {enable_write_ahead_logging_mode, false}}, {23, {create_metadata_table, true}}, {24, {add_not_null_constraint_to_drafts, true}}, {25, {add_not_null_constraint_to_metadata, true}}, {26, {add_avatar_column_to_threads_table, true}}, {27, {add_pinned_count_column_to_threads, true}}, {28, {create_message_store_threads_table, true}}, {29, {create_reports_table, true}}, {30, {create_persist_storage_table, true}}, {31, {recreate_message_store_threads_table, true}}, {32, {create_users_table, true}}, {33, {create_keyservers_table, true}}, {34, {enable_rollback_journal_mode, false}}, {35, {create_communities_table, true}}}}; enum class MigrationResult { SUCCESS, FAILURE, NOT_APPLIED }; MigrationResult applyMigrationWithTransaction( sqlite3 *db, const MigrateFunction &migrate, int index) { sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); auto db_version = get_database_version(db); if (index <= db_version) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return MigrationResult::NOT_APPLIED; } auto rc = migrate(db); if (!rc) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return MigrationResult::FAILURE; } auto database_version_set = set_database_version(db, index); if (!database_version_set) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return MigrationResult::FAILURE; } sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, nullptr); return MigrationResult::SUCCESS; } MigrationResult applyMigrationWithoutTransaction( sqlite3 *db, const MigrateFunction &migrate, int index) { auto db_version = get_database_version(db); if (index <= db_version) { return MigrationResult::NOT_APPLIED; } auto rc = migrate(db); if (!rc) { return MigrationResult::FAILURE; } sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); auto inner_db_version = get_database_version(db); if (index <= inner_db_version) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return MigrationResult::NOT_APPLIED; } auto database_version_set = set_database_version(db, index); if (!database_version_set) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return MigrationResult::FAILURE; } sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, nullptr); return MigrationResult::SUCCESS; } bool set_up_database(sqlite3 *db) { sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); auto db_version = get_database_version(db); auto latest_version = migrations.back().first; if (db_version == latest_version) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return true; } if (db_version != 0 || !create_schema(db) || !set_database_version(db, latest_version)) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); return false; } sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, nullptr); return true; } void SQLiteQueryExecutor::migrate() { // We don't want to run `PRAGMA key = ...;` // on main web database. The context is here: // https://linear.app/comm/issue/ENG-6398/issues-with-sqlcipher-on-web #ifndef EMSCRIPTEN validate_encryption(); #endif sqlite3 *db; sqlite3_open(SQLiteQueryExecutor::sqliteFilePath.c_str(), &db); default_on_db_open_callback(db); std::stringstream db_path; db_path << "db path: " << SQLiteQueryExecutor::sqliteFilePath.c_str() << std::endl; Logger::log(db_path.str()); auto db_version = get_database_version(db); std::stringstream version_msg; version_msg << "db version: " << db_version << std::endl; Logger::log(version_msg.str()); if (db_version == 0) { auto db_created = set_up_database(db); if (!db_created) { sqlite3_close(db); Logger::log("Database structure creation error."); throw std::runtime_error("Database structure creation error"); } Logger::log("Database structure created."); sqlite3_close(db); return; } for (const auto &[idx, migration] : migrations) { const auto &[applyMigration, shouldBeInTransaction] = migration; MigrationResult migrationResult; if (shouldBeInTransaction) { migrationResult = applyMigrationWithTransaction(db, applyMigration, idx); } else { migrationResult = applyMigrationWithoutTransaction(db, applyMigration, idx); } if (migrationResult == MigrationResult::NOT_APPLIED) { continue; } std::stringstream migration_msg; if (migrationResult == MigrationResult::FAILURE) { migration_msg << "migration " << idx << " failed." << std::endl; Logger::log(migration_msg.str()); sqlite3_close(db); throw std::runtime_error(migration_msg.str()); } if (migrationResult == MigrationResult::SUCCESS) { migration_msg << "migration " << idx << " succeeded." << std::endl; Logger::log(migration_msg.str()); } } sqlite3_close(db); } SQLiteQueryExecutor::SQLiteQueryExecutor() { SQLiteQueryExecutor::migrate(); #ifndef EMSCRIPTEN std::string currentBackupID = this->getMetadata("backupID"); if (!StaffUtils::isStaffRelease() || !currentBackupID.size()) { return; } SQLiteQueryExecutor::connectionManager.setLogsMonitoring(true); #endif } SQLiteQueryExecutor::SQLiteQueryExecutor(std::string sqliteFilePath) { SQLiteQueryExecutor::sqliteFilePath = sqliteFilePath; SQLiteQueryExecutor::migrate(); } sqlite3 *SQLiteQueryExecutor::getConnection() { if (SQLiteQueryExecutor::connectionManager.getConnection()) { return SQLiteQueryExecutor::connectionManager.getConnection(); } SQLiteQueryExecutor::connectionManager.initializeConnection( SQLiteQueryExecutor::sqliteFilePath, default_on_db_open_callback); return SQLiteQueryExecutor::connectionManager.getConnection(); } void SQLiteQueryExecutor::closeConnection() { SQLiteQueryExecutor::connectionManager.closeConnection(); } SQLiteQueryExecutor::~SQLiteQueryExecutor() { SQLiteQueryExecutor::closeConnection(); } std::string SQLiteQueryExecutor::getDraft(std::string key) const { static std::string getDraftByPrimaryKeySQL = "SELECT * " "FROM drafts " "WHERE key = ?;"; std::unique_ptr draft = getEntityByPrimaryKey( SQLiteQueryExecutor::getConnection(), getDraftByPrimaryKeySQL, key); return (draft == nullptr) ? "" : draft->text; } std::unique_ptr SQLiteQueryExecutor::getThread(std::string threadID) const { static std::string getThreadByPrimaryKeySQL = "SELECT * " "FROM threads " "WHERE id = ?;"; return getEntityByPrimaryKey( SQLiteQueryExecutor::getConnection(), getThreadByPrimaryKeySQL, threadID); } void SQLiteQueryExecutor::updateDraft(std::string key, std::string text) const { static std::string replaceDraftSQL = "REPLACE INTO drafts (key, text) " "VALUES (?, ?);"; Draft draft = {key, text}; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceDraftSQL, draft); } bool SQLiteQueryExecutor::moveDraft(std::string oldKey, std::string newKey) const { std::string draftText = this->getDraft(oldKey); if (!draftText.size()) { return false; } static std::string rekeyDraftSQL = "UPDATE OR REPLACE drafts " "SET key = ? " "WHERE key = ?;"; rekeyAllEntities( SQLiteQueryExecutor::getConnection(), rekeyDraftSQL, oldKey, newKey); return true; } std::vector SQLiteQueryExecutor::getAllDrafts() const { static std::string getAllDraftsSQL = "SELECT * " "FROM drafts;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllDraftsSQL); } void SQLiteQueryExecutor::removeAllDrafts() const { static std::string removeAllDraftsSQL = "DELETE FROM drafts;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllDraftsSQL); } void SQLiteQueryExecutor::removeDrafts( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeDraftsByKeysSQLStream; removeDraftsByKeysSQLStream << "DELETE FROM drafts " "WHERE key IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeDraftsByKeysSQLStream.str(), ids); } void SQLiteQueryExecutor::removeAllMessages() const { static std::string removeAllMessagesSQL = "DELETE FROM messages;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllMessagesSQL); } std::vector>> SQLiteQueryExecutor::getAllMessages() const { static std::string getAllMessagesSQL = "SELECT * " "FROM messages " "LEFT JOIN media " " ON messages.id = media.container " "ORDER BY messages.id;"; SQLiteStatementWrapper preparedSQL( SQLiteQueryExecutor::getConnection(), getAllMessagesSQL, "Failed to retrieve all messages."); std::string prevMsgIdx{}; std::vector>> allMessages; for (int stepResult = sqlite3_step(preparedSQL); stepResult == SQLITE_ROW; stepResult = sqlite3_step(preparedSQL)) { Message message = Message::fromSQLResult(preparedSQL, 0); if (message.id == prevMsgIdx) { allMessages.back().second.push_back(Media::fromSQLResult(preparedSQL, 8)); } else { prevMsgIdx = message.id; std::vector mediaForMsg; if (sqlite3_column_type(preparedSQL, 8) != SQLITE_NULL) { mediaForMsg.push_back(Media::fromSQLResult(preparedSQL, 8)); } allMessages.push_back(std::make_pair(std::move(message), mediaForMsg)); } } return allMessages; } void SQLiteQueryExecutor::removeMessages( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeMessagesByKeysSQLStream; removeMessagesByKeysSQLStream << "DELETE FROM messages " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMessagesByKeysSQLStream.str(), ids); } void SQLiteQueryExecutor::removeMessagesForThreads( const std::vector &threadIDs) const { if (!threadIDs.size()) { return; } std::stringstream removeMessagesByKeysSQLStream; removeMessagesByKeysSQLStream << "DELETE FROM messages " "WHERE thread IN " << getSQLStatementArray(threadIDs.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMessagesByKeysSQLStream.str(), threadIDs); } void SQLiteQueryExecutor::replaceMessage(const Message &message) const { static std::string replaceMessageSQL = "REPLACE INTO messages " "(id, local_id, thread, user, type, future_type, content, time) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceMessageSQL, message); } void SQLiteQueryExecutor::rekeyMessage(std::string from, std::string to) const { static std::string rekeyMessageSQL = "UPDATE OR REPLACE messages " "SET id = ? " "WHERE id = ?"; rekeyAllEntities( SQLiteQueryExecutor::getConnection(), rekeyMessageSQL, from, to); } void SQLiteQueryExecutor::removeAllMedia() const { static std::string removeAllMediaSQL = "DELETE FROM media;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllMediaSQL); } void SQLiteQueryExecutor::removeMediaForMessages( const std::vector &msg_ids) const { if (!msg_ids.size()) { return; } std::stringstream removeMediaByKeysSQLStream; removeMediaByKeysSQLStream << "DELETE FROM media " "WHERE container IN " << getSQLStatementArray(msg_ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMediaByKeysSQLStream.str(), msg_ids); } void SQLiteQueryExecutor::removeMediaForMessage(std::string msg_id) const { static std::string removeMediaByKeySQL = "DELETE FROM media " "WHERE container IN (?);"; std::vector keys = {msg_id}; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMediaByKeySQL, keys); } void SQLiteQueryExecutor::removeMediaForThreads( const std::vector &thread_ids) const { if (!thread_ids.size()) { return; } std::stringstream removeMediaByKeysSQLStream; removeMediaByKeysSQLStream << "DELETE FROM media " "WHERE thread IN " << getSQLStatementArray(thread_ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMediaByKeysSQLStream.str(), thread_ids); } void SQLiteQueryExecutor::replaceMedia(const Media &media) const { static std::string replaceMediaSQL = "REPLACE INTO media " "(id, container, thread, uri, type, extras) " "VALUES (?, ?, ?, ?, ?, ?)"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceMediaSQL, media); } void SQLiteQueryExecutor::rekeyMediaContainers(std::string from, std::string to) const { static std::string rekeyMediaContainersSQL = "UPDATE media SET container = ? WHERE container = ?;"; rekeyAllEntities( SQLiteQueryExecutor::getConnection(), rekeyMediaContainersSQL, from, to); } void SQLiteQueryExecutor::replaceMessageStoreThreads( const std::vector &threads) const { static std::string replaceMessageStoreThreadSQL = "REPLACE INTO message_store_threads " "(id, start_reached) " "VALUES (?, ?);"; for (auto &thread : threads) { replaceEntity( SQLiteQueryExecutor::getConnection(), replaceMessageStoreThreadSQL, thread); } } void SQLiteQueryExecutor::removeAllMessageStoreThreads() const { static std::string removeAllMessageStoreThreadsSQL = "DELETE FROM message_store_threads;"; removeAllEntities( SQLiteQueryExecutor::getConnection(), removeAllMessageStoreThreadsSQL); } void SQLiteQueryExecutor::removeMessageStoreThreads( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeMessageStoreThreadsByKeysSQLStream; removeMessageStoreThreadsByKeysSQLStream << "DELETE FROM message_store_threads " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMessageStoreThreadsByKeysSQLStream.str(), ids); } std::vector SQLiteQueryExecutor::getAllMessageStoreThreads() const { static std::string getAllMessageStoreThreadsSQL = "SELECT * " "FROM message_store_threads;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllMessageStoreThreadsSQL); } std::vector SQLiteQueryExecutor::getAllThreads() const { static std::string getAllThreadsSQL = "SELECT * " "FROM threads;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllThreadsSQL); }; void SQLiteQueryExecutor::removeThreads(std::vector ids) const { if (!ids.size()) { return; } std::stringstream removeThreadsByKeysSQLStream; removeThreadsByKeysSQLStream << "DELETE FROM threads " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeThreadsByKeysSQLStream.str(), ids); }; void SQLiteQueryExecutor::replaceThread(const Thread &thread) const { static std::string replaceThreadSQL = "REPLACE INTO threads (" " id, type, name, description, color, creation_time, parent_thread_id," " containing_thread_id, community, members, roles, current_user," " source_message_id, replies_count, avatar, pinned_count) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceThreadSQL, thread); }; void SQLiteQueryExecutor::removeAllThreads() const { static std::string removeAllThreadsSQL = "DELETE FROM threads;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllThreadsSQL); }; void SQLiteQueryExecutor::replaceReport(const Report &report) const { static std::string replaceReportSQL = "REPLACE INTO reports (id, report) " "VALUES (?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceReportSQL, report); } void SQLiteQueryExecutor::removeAllReports() const { static std::string removeAllReportsSQL = "DELETE FROM reports;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllReportsSQL); } void SQLiteQueryExecutor::removeReports( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeReportsByKeysSQLStream; removeReportsByKeysSQLStream << "DELETE FROM reports " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeReportsByKeysSQLStream.str(), ids); } std::vector SQLiteQueryExecutor::getAllReports() const { static std::string getAllReportsSQL = "SELECT * " "FROM reports;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllReportsSQL); } void SQLiteQueryExecutor::setPersistStorageItem( std::string key, std::string item) const { static std::string replacePersistStorageItemSQL = "REPLACE INTO persist_storage (key, item) " "VALUES (?, ?);"; PersistItem entry{ key, item, }; replaceEntity( SQLiteQueryExecutor::getConnection(), replacePersistStorageItemSQL, entry); } void SQLiteQueryExecutor::removePersistStorageItem(std::string key) const { static std::string removePersistStorageItemByKeySQL = "DELETE FROM persist_storage " "WHERE key IN (?);"; std::vector keys = {key}; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removePersistStorageItemByKeySQL, keys); } std::string SQLiteQueryExecutor::getPersistStorageItem(std::string key) const { static std::string getPersistStorageItemByPrimaryKeySQL = "SELECT * " "FROM persist_storage " "WHERE key = ?;"; std::unique_ptr entry = getEntityByPrimaryKey( SQLiteQueryExecutor::getConnection(), getPersistStorageItemByPrimaryKeySQL, key); return (entry == nullptr) ? "" : entry->item; } void SQLiteQueryExecutor::replaceUser(const UserInfo &user_info) const { static std::string replaceUserSQL = "REPLACE INTO users (id, user_info) " "VALUES (?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceUserSQL, user_info); } void SQLiteQueryExecutor::removeAllUsers() const { static std::string removeAllUsersSQL = "DELETE FROM users;"; removeAllEntities(SQLiteQueryExecutor::getConnection(), removeAllUsersSQL); } void SQLiteQueryExecutor::removeUsers( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeUsersByKeysSQLStream; removeUsersByKeysSQLStream << "DELETE FROM users " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeUsersByKeysSQLStream.str(), ids); } void SQLiteQueryExecutor::replaceKeyserver( const KeyserverInfo &keyserver_info) const { static std::string replaceKeyserverSQL = "REPLACE INTO keyservers (id, keyserver_info) " "VALUES (?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceKeyserverSQL, keyserver_info); } void SQLiteQueryExecutor::removeAllKeyservers() const { static std::string removeAllKeyserversSQL = "DELETE FROM keyservers;"; removeAllEntities( SQLiteQueryExecutor::getConnection(), removeAllKeyserversSQL); } void SQLiteQueryExecutor::removeKeyservers( const std::vector &ids) const { if (!ids.size()) { return; } std::stringstream removeKeyserversByKeysSQLStream; removeKeyserversByKeysSQLStream << "DELETE FROM keyservers " "WHERE id IN " << getSQLStatementArray(ids.size()) << ";"; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeKeyserversByKeysSQLStream.str(), ids); } std::vector SQLiteQueryExecutor::getAllKeyservers() const { static std::string getAllKeyserversSQL = "SELECT * " "FROM keyservers;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllKeyserversSQL); } std::vector SQLiteQueryExecutor::getAllUsers() const { static std::string getAllUsersSQL = "SELECT * " "FROM users;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllUsersSQL); } +void SQLiteQueryExecutor::replaceCommunity( + const CommunityInfo &community_info) const { + static std::string replaceCommunitySQL = + "REPLACE INTO communities (id, community_info) " + "VALUES (?, ?);"; + replaceEntity( + SQLiteQueryExecutor::getConnection(), + replaceCommunitySQL, + community_info); +} + +void SQLiteQueryExecutor::removeAllCommunities() const { + static std::string removeAllCommunitiesSQL = "DELETE FROM communities;"; + removeAllEntities( + SQLiteQueryExecutor::getConnection(), removeAllCommunitiesSQL); +} + +void SQLiteQueryExecutor::removeCommunities( + const std::vector &ids) const { + if (!ids.size()) { + return; + } + + std::stringstream removeCommunitiesByKeysSQLStream; + removeCommunitiesByKeysSQLStream << "DELETE FROM communities " + "WHERE id IN " + << getSQLStatementArray(ids.size()) << ";"; + + removeEntitiesByKeys( + SQLiteQueryExecutor::getConnection(), + removeCommunitiesByKeysSQLStream.str(), + ids); +} + +std::vector SQLiteQueryExecutor::getAllCommunities() const { + static std::string getAllCommunitiesSQL = + "SELECT * " + "FROM communities;"; + return getAllEntities( + SQLiteQueryExecutor::getConnection(), getAllCommunitiesSQL); +} + void SQLiteQueryExecutor::beginTransaction() const { executeQuery(SQLiteQueryExecutor::getConnection(), "BEGIN TRANSACTION;"); } void SQLiteQueryExecutor::commitTransaction() const { executeQuery(SQLiteQueryExecutor::getConnection(), "COMMIT;"); } void SQLiteQueryExecutor::rollbackTransaction() const { executeQuery(SQLiteQueryExecutor::getConnection(), "ROLLBACK;"); } std::vector SQLiteQueryExecutor::getOlmPersistSessionsData() const { static std::string getAllOlmPersistSessionsSQL = "SELECT * " "FROM olm_persist_sessions;"; return getAllEntities( SQLiteQueryExecutor::getConnection(), getAllOlmPersistSessionsSQL); } std::optional SQLiteQueryExecutor::getOlmPersistAccountData() const { static std::string getAllOlmPersistAccountSQL = "SELECT * " "FROM olm_persist_account;"; std::vector result = getAllEntities( SQLiteQueryExecutor::getConnection(), getAllOlmPersistAccountSQL); if (result.size() > 1) { throw std::system_error( ECANCELED, std::generic_category(), "Multiple records found for the olm_persist_account table"); } return (result.size() == 0) ? std::nullopt : std::optional(result[0].account_data); } void SQLiteQueryExecutor::storeOlmPersistAccount( const std::string &accountData) const { static std::string replaceOlmPersistAccountSQL = "REPLACE INTO olm_persist_account (id, account_data) " "VALUES (?, ?);"; OlmPersistAccount persistAccount = {ACCOUNT_ID, accountData}; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceOlmPersistAccountSQL, persistAccount); } void SQLiteQueryExecutor::storeOlmPersistSession( const OlmPersistSession &session) const { static std::string replaceOlmPersistSessionSQL = "REPLACE INTO olm_persist_sessions (target_user_id, session_data) " "VALUES (?, ?);"; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceOlmPersistSessionSQL, session); } void SQLiteQueryExecutor::storeOlmPersistData(crypto::Persist persist) const { std::string accountData = std::string(persist.account.begin(), persist.account.end()); this->storeOlmPersistAccount(accountData); for (auto it = persist.sessions.begin(); it != persist.sessions.end(); it++) { OlmPersistSession persistSession = { it->first, std::string(it->second.begin(), it->second.end())}; this->storeOlmPersistSession(persistSession); } } void SQLiteQueryExecutor::setNotifyToken(std::string token) const { this->setMetadata("notify_token", token); } void SQLiteQueryExecutor::clearNotifyToken() const { this->clearMetadata("notify_token"); } void SQLiteQueryExecutor::setCurrentUserID(std::string userID) const { this->setMetadata("current_user_id", userID); } std::string SQLiteQueryExecutor::getCurrentUserID() const { return this->getMetadata("current_user_id"); } void SQLiteQueryExecutor::setMetadata(std::string entry_name, std::string data) const { std::string replaceMetadataSQL = "REPLACE INTO metadata (name, data) " "VALUES (?, ?);"; Metadata entry{ entry_name, data, }; replaceEntity( SQLiteQueryExecutor::getConnection(), replaceMetadataSQL, entry); } void SQLiteQueryExecutor::clearMetadata(std::string entry_name) const { static std::string removeMetadataByKeySQL = "DELETE FROM metadata " "WHERE name IN (?);"; std::vector keys = {entry_name}; removeEntitiesByKeys( SQLiteQueryExecutor::getConnection(), removeMetadataByKeySQL, keys); } std::string SQLiteQueryExecutor::getMetadata(std::string entry_name) const { std::string getMetadataByPrimaryKeySQL = "SELECT * " "FROM metadata " "WHERE name = ?;"; std::unique_ptr entry = getEntityByPrimaryKey( SQLiteQueryExecutor::getConnection(), getMetadataByPrimaryKeySQL, entry_name); return (entry == nullptr) ? "" : entry->data; } #ifdef EMSCRIPTEN std::vector SQLiteQueryExecutor::getAllThreadsWeb() const { auto threads = this->getAllThreads(); std::vector webThreads; webThreads.reserve(threads.size()); for (const auto &thread : threads) { webThreads.emplace_back(thread); } return webThreads; }; void SQLiteQueryExecutor::replaceThreadWeb(const WebThread &thread) const { this->replaceThread(thread.toThread()); }; std::vector SQLiteQueryExecutor::getAllMessagesWeb() const { auto allMessages = this->getAllMessages(); std::vector allMessageWithMedias; for (auto &messageWitMedia : allMessages) { allMessageWithMedias.push_back( {std::move(messageWitMedia.first), messageWitMedia.second}); } return allMessageWithMedias; } void SQLiteQueryExecutor::replaceMessageWeb(const WebMessage &message) const { this->replaceMessage(message.toMessage()); }; NullableString SQLiteQueryExecutor::getOlmPersistAccountDataWeb() const { std::optional accountData = this->getOlmPersistAccountData(); if (!accountData.has_value()) { return NullableString(); } return std::make_unique(accountData.value()); } #else void SQLiteQueryExecutor::clearSensitiveData() { SQLiteQueryExecutor::closeConnection(); if (file_exists(SQLiteQueryExecutor::sqliteFilePath) && std::remove(SQLiteQueryExecutor::sqliteFilePath.c_str())) { std::ostringstream errorStream; errorStream << "Failed to delete database file. Details: " << strerror(errno); throw std::system_error(errno, std::generic_category(), errorStream.str()); } SQLiteQueryExecutor::generateFreshEncryptionKey(); SQLiteQueryExecutor::migrate(); } void SQLiteQueryExecutor::initialize(std::string &databasePath) { std::call_once(SQLiteQueryExecutor::initialized, [&databasePath]() { SQLiteQueryExecutor::sqliteFilePath = databasePath; folly::Optional maybeEncryptionKey = CommSecureStore::get(SQLiteQueryExecutor::secureStoreEncryptionKeyID); folly::Optional maybeBackupLogsEncryptionKey = CommSecureStore::get( SQLiteQueryExecutor::secureStoreBackupLogsEncryptionKeyID); if (file_exists(databasePath) && maybeEncryptionKey && maybeBackupLogsEncryptionKey) { SQLiteQueryExecutor::encryptionKey = maybeEncryptionKey.value(); SQLiteQueryExecutor::backupLogsEncryptionKey = maybeBackupLogsEncryptionKey.value(); return; } else if (file_exists(databasePath) && maybeEncryptionKey) { SQLiteQueryExecutor::encryptionKey = maybeEncryptionKey.value(); SQLiteQueryExecutor::generateFreshBackupLogsEncryptionKey(); return; } SQLiteQueryExecutor::generateFreshEncryptionKey(); }); } void SQLiteQueryExecutor::createMainCompaction(std::string backupID) const { std::string finalBackupPath = PlatformSpecificTools::getBackupFilePath(backupID, false); std::string finalAttachmentsPath = PlatformSpecificTools::getBackupFilePath(backupID, true); std::string tempBackupPath = finalBackupPath + "_tmp"; std::string tempAttachmentsPath = finalAttachmentsPath + "_tmp"; if (file_exists(tempBackupPath)) { Logger::log( "Attempting to delete temporary backup file from previous backup " "attempt."); attempt_delete_file( tempBackupPath, "Failed to delete temporary backup file from previous backup attempt."); } if (file_exists(tempAttachmentsPath)) { Logger::log( "Attempting to delete temporary attachments file from previous backup " "attempt."); attempt_delete_file( tempAttachmentsPath, "Failed to delete temporary attachments file from previous backup " "attempt."); } sqlite3 *backupDB; sqlite3_open(tempBackupPath.c_str(), &backupDB); set_encryption_key(backupDB); sqlite3_backup *backupObj = sqlite3_backup_init( backupDB, "main", SQLiteQueryExecutor::getConnection(), "main"); if (!backupObj) { std::stringstream error_message; error_message << "Failed to init backup for main compaction. Details: " << sqlite3_errmsg(backupDB) << std::endl; sqlite3_close(backupDB); throw std::runtime_error(error_message.str()); } int backupResult = sqlite3_backup_step(backupObj, -1); sqlite3_backup_finish(backupObj); if (backupResult == SQLITE_BUSY || backupResult == SQLITE_LOCKED) { sqlite3_close(backupDB); throw std::runtime_error( "Programmer error. Database in transaction during backup attempt."); } else if (backupResult != SQLITE_DONE) { sqlite3_close(backupDB); std::stringstream error_message; error_message << "Failed to create database backup. Details: " << sqlite3_errstr(backupResult); throw std::runtime_error(error_message.str()); } std::string removeDeviceSpecificDataSQL = "DELETE FROM olm_persist_account;" "DELETE FROM olm_persist_sessions;" "DELETE FROM metadata;"; executeQuery(backupDB, removeDeviceSpecificDataSQL); executeQuery(backupDB, "VACUUM;"); sqlite3_close(backupDB); attempt_rename_file( tempBackupPath, finalBackupPath, "Failed to rename complete temporary backup file to final backup file."); std::ofstream tempAttachmentsFile(tempAttachmentsPath); if (!tempAttachmentsFile.is_open()) { throw std::runtime_error( "Unable to create attachments file for backup id: " + backupID); } std::string getAllBlobServiceMediaSQL = "SELECT * FROM media WHERE uri LIKE 'comm-blob-service://%';"; std::vector blobServiceMedia = getAllEntities( SQLiteQueryExecutor::getConnection(), getAllBlobServiceMediaSQL); for (const auto &media : blobServiceMedia) { std::string blobServiceURI = media.uri; std::string blobHash = blob_hash_from_blob_service_uri(blobServiceURI); tempAttachmentsFile << blobHash << "\n"; } tempAttachmentsFile.close(); attempt_rename_file( tempAttachmentsPath, finalAttachmentsPath, "Failed to rename complete temporary attachments file to final " "attachments file."); this->setMetadata("backupID", backupID); this->clearMetadata("logID"); if (StaffUtils::isStaffRelease()) { SQLiteQueryExecutor::connectionManager.setLogsMonitoring(true); } } void SQLiteQueryExecutor::generateFreshEncryptionKey() { std::string encryptionKey = comm::crypto::Tools::generateRandomHexString( SQLiteQueryExecutor::sqlcipherEncryptionKeySize); CommSecureStore::set( SQLiteQueryExecutor::secureStoreEncryptionKeyID, encryptionKey); SQLiteQueryExecutor::encryptionKey = encryptionKey; SQLiteQueryExecutor::generateFreshBackupLogsEncryptionKey(); } void SQLiteQueryExecutor::generateFreshBackupLogsEncryptionKey() { std::string backupLogsEncryptionKey = comm::crypto::Tools::generateRandomHexString( SQLiteQueryExecutor::backupLogsEncryptionKeySize); CommSecureStore::set( SQLiteQueryExecutor::secureStoreBackupLogsEncryptionKeyID, backupLogsEncryptionKey); SQLiteQueryExecutor::backupLogsEncryptionKey = backupLogsEncryptionKey; } void SQLiteQueryExecutor::captureBackupLogs() const { std::string backupID = this->getMetadata("backupID"); if (!backupID.size()) { return; } std::string logID = this->getMetadata("logID"); if (!logID.size()) { logID = "1"; } bool newLogCreated = SQLiteQueryExecutor::connectionManager.captureLogs( backupID, logID, SQLiteQueryExecutor::backupLogsEncryptionKey); if (!newLogCreated) { return; } this->setMetadata("logID", std::to_string(std::stoi(logID) + 1)); } #endif void SQLiteQueryExecutor::restoreFromMainCompaction( std::string mainCompactionPath, std::string mainCompactionEncryptionKey) const { if (!file_exists(mainCompactionPath)) { throw std::runtime_error("Restore attempt but backup file does not exist."); } sqlite3 *backupDB; if (!is_database_queryable( backupDB, true, mainCompactionPath, mainCompactionEncryptionKey)) { throw std::runtime_error("Backup file or encryption key corrupted."); } // We don't want to run `PRAGMA key = ...;` // on main web database. The context is here: // https://linear.app/comm/issue/ENG-6398/issues-with-sqlcipher-on-web #ifdef EMSCRIPTEN std::string plaintextBackupPath = mainCompactionPath + "_plaintext"; if (file_exists(plaintextBackupPath)) { attempt_delete_file( plaintextBackupPath, "Failed to delete plaintext backup file from previous backup attempt."); } std::string plaintextMigrationDBQuery = "PRAGMA key = \"x'" + mainCompactionEncryptionKey + "'\";" "ATTACH DATABASE '" + plaintextBackupPath + "' AS plaintext KEY '';" "SELECT sqlcipher_export('plaintext');" "DETACH DATABASE plaintext;"; sqlite3_open(mainCompactionPath.c_str(), &backupDB); char *plaintextMigrationErr; sqlite3_exec( backupDB, plaintextMigrationDBQuery.c_str(), nullptr, nullptr, &plaintextMigrationErr); sqlite3_close(backupDB); if (plaintextMigrationErr) { std::stringstream error_message; error_message << "Failed to migrate backup SQLCipher file to plaintext " "SQLite file. Details" << plaintextMigrationErr << std::endl; std::string error_message_str = error_message.str(); sqlite3_free(plaintextMigrationErr); throw std::runtime_error(error_message_str); } sqlite3_open(plaintextBackupPath.c_str(), &backupDB); #else sqlite3_open(mainCompactionPath.c_str(), &backupDB); set_encryption_key(backupDB, mainCompactionEncryptionKey); #endif sqlite3_backup *backupObj = sqlite3_backup_init( SQLiteQueryExecutor::getConnection(), "main", backupDB, "main"); if (!backupObj) { std::stringstream error_message; error_message << "Failed to init backup for main compaction. Details: " << sqlite3_errmsg(SQLiteQueryExecutor::getConnection()) << std::endl; sqlite3_close(backupDB); throw std::runtime_error(error_message.str()); } int backupResult = sqlite3_backup_step(backupObj, -1); sqlite3_backup_finish(backupObj); sqlite3_close(backupDB); if (backupResult == SQLITE_BUSY || backupResult == SQLITE_LOCKED) { throw std::runtime_error( "Programmer error. Database in transaction during restore attempt."); } else if (backupResult != SQLITE_DONE) { std::stringstream error_message; error_message << "Failed to restore database from backup. Details: " << sqlite3_errstr(backupResult); throw std::runtime_error(error_message.str()); } #ifdef EMSCRIPTEN attempt_delete_file( plaintextBackupPath, "Failed to delete plaintext compaction file after successful restore."); #endif attempt_delete_file( mainCompactionPath, "Failed to delete main compaction file after successful restore."); } void SQLiteQueryExecutor::restoreFromBackupLog( const std::vector &backupLog) const { SQLiteQueryExecutor::connectionManager.restoreFromBackupLog(backupLog); } } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h index 80e78090d..f93e61f92 100644 --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h @@ -1,125 +1,130 @@ #pragma once #include "../CryptoTools/Persist.h" #include "DatabaseQueryExecutor.h" #include "NativeSQLiteConnectionManager.h" +#include "entities/CommunityInfo.h" #include "entities/Draft.h" #include "entities/KeyserverInfo.h" #include "entities/UserInfo.h" #include #include namespace comm { class SQLiteQueryExecutor : public DatabaseQueryExecutor { static void migrate(); static sqlite3 *getConnection(); static void closeConnection(); static std::once_flag initialized; static int sqlcipherEncryptionKeySize; static std::string secureStoreEncryptionKeyID; static int backupLogsEncryptionKeySize; static std::string secureStoreBackupLogsEncryptionKeyID; static std::string backupLogsEncryptionKey; #ifndef EMSCRIPTEN static NativeSQLiteConnectionManager connectionManager; static void generateFreshEncryptionKey(); static void generateFreshBackupLogsEncryptionKey(); #else static SQLiteConnectionManager connectionManager; #endif public: static std::string sqliteFilePath; static std::string encryptionKey; SQLiteQueryExecutor(); ~SQLiteQueryExecutor(); SQLiteQueryExecutor(std::string sqliteFilePath); 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 removeDrafts(const std::vector &ids) 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 replaceMessageStoreThreads( const std::vector &threads) const override; void removeMessageStoreThreads(const std::vector &ids) const override; void removeAllMessageStoreThreads() const override; std::vector getAllMessageStoreThreads() 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 replaceReport(const Report &report) const override; void removeReports(const std::vector &ids) const override; void removeAllReports() const override; std::vector getAllReports() const override; void setPersistStorageItem(std::string key, std::string item) const override; void removePersistStorageItem(std::string key) const override; std::string getPersistStorageItem(std::string key) const override; void replaceUser(const UserInfo &user_info) const override; void removeUsers(const std::vector &ids) const override; void removeAllUsers() const override; std::vector getAllUsers() const override; void replaceKeyserver(const KeyserverInfo &keyserver_info) const override; void removeKeyservers(const std::vector &ids) const override; void removeAllKeyservers() const override; std::vector getAllKeyservers() const override; + void replaceCommunity(const CommunityInfo &community_info) const override; + void removeCommunities(const std::vector &ids) const override; + void removeAllCommunities() const override; + std::vector getAllCommunities() const override; void beginTransaction() const override; void commitTransaction() const override; void rollbackTransaction() const override; std::vector getOlmPersistSessionsData() const override; std::optional getOlmPersistAccountData() const override; void storeOlmPersistSession(const OlmPersistSession &session) const override; void storeOlmPersistAccount(const std::string &accountData) 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 setMetadata(std::string entry_name, std::string data) const override; void clearMetadata(std::string entry_name) const override; std::string getMetadata(std::string entry_name) const override; void restoreFromMainCompaction( std::string mainCompactionPath, std::string mainCompactionEncryptionKey) const override; void restoreFromBackupLog( const std::vector &backupLog) const override; #ifdef EMSCRIPTEN std::vector getAllThreadsWeb() const override; void replaceThreadWeb(const WebThread &thread) const override; std::vector getAllMessagesWeb() const override; void replaceMessageWeb(const WebMessage &message) const override; NullableString getOlmPersistAccountDataWeb() const override; #else static void clearSensitiveData(); static void initialize(std::string &databasePath); void createMainCompaction(std::string backupID) const override; void captureBackupLogs() const override; #endif }; } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/entities/CommunityInfo.h b/native/cpp/CommonCpp/DatabaseManagers/entities/CommunityInfo.h new file mode 100644 index 000000000..6764d4c34 --- /dev/null +++ b/native/cpp/CommonCpp/DatabaseManagers/entities/CommunityInfo.h @@ -0,0 +1,24 @@ +#pragma once + +#include "SQLiteDataConverters.h" +#include +#include + +namespace comm { + +struct CommunityInfo { + std::string id; + std::string community_info; + + static CommunityInfo fromSQLResult(sqlite3_stmt *sqlRow, int idx) { + return CommunityInfo{ + getStringFromSQLRow(sqlRow, idx), getStringFromSQLRow(sqlRow, idx + 1)}; + } + + int bindToSQL(sqlite3_stmt *sql, int idx) const { + bindStringToSQL(id, sql, idx); + return bindStringToSQL(community_info, sql, idx + 1); + } +}; + +} // namespace comm diff --git a/web/cpp/SQLiteQueryExecutorBindings.cpp b/web/cpp/SQLiteQueryExecutorBindings.cpp index 9150b6af8..163180261 100644 --- a/web/cpp/SQLiteQueryExecutorBindings.cpp +++ b/web/cpp/SQLiteQueryExecutorBindings.cpp @@ -1,218 +1,226 @@ #include "SQLiteQueryExecutor.cpp" #include "entities/Nullable.h" #include #include namespace comm { using namespace emscripten; std::string getExceptionMessage(int exceptionPtr) { if (exceptionPtr == 0) { return std::string("Exception pointer value was null"); } std::exception *e = reinterpret_cast(exceptionPtr); if (e) { return std::string(e->what()); } return std::string("Pointer to exception was invalid"); } EMSCRIPTEN_BINDINGS(SQLiteQueryExecutor) { function("getExceptionMessage", &getExceptionMessage); value_object("NullableString") .field("value", &NullableString::value) .field("isNull", &NullableString::isNull); value_object("NullableInt") .field("value", &NullableInt::value) .field("isNull", &NullableInt::isNull); value_object("Draft") .field("key", &Draft::key) .field("text", &Draft::text); value_object("Report") .field("id", &Report::id) .field("report", &Report::report); value_object("PersistItem") .field("key", &PersistItem::key) .field("item", &PersistItem::item); value_object("UserInfo") .field("id", &UserInfo::id) .field("userInfo", &UserInfo::user_info); value_object("KeyserverInfo") .field("id", &KeyserverInfo::id) .field("keyserverInfo", &KeyserverInfo::keyserver_info); value_object("MessageStoreThreads") .field("id", &MessageStoreThread::id) .field("startReached", &MessageStoreThread::start_reached); + value_object("CommunityInfo") + .field("id", &CommunityInfo::id) + .field("communityInfo", &CommunityInfo::community_info); value_object("WebThread") .field("id", &WebThread::id) .field("type", &WebThread::type) .field("name", &WebThread::name) .field("description", &WebThread::description) .field("color", &WebThread::color) .field("creationTime", &WebThread::creation_time) .field("parentThreadID", &WebThread::parent_thread_id) .field("containingThreadID", &WebThread::containing_thread_id) .field("community", &WebThread::community) .field("members", &WebThread::members) .field("roles", &WebThread::roles) .field("currentUser", &WebThread::current_user) .field("sourceMessageID", &WebThread::source_message_id) .field("repliesCount", &WebThread::replies_count) .field("avatar", &WebThread::avatar) .field("pinnedCount", &WebThread::pinned_count); value_object("WebMessage") .field("id", &WebMessage::id) .field("localID", &WebMessage::local_id) .field("thread", &WebMessage::thread) .field("user", &WebMessage::user) .field("type", &WebMessage::type) .field("futureType", &WebMessage::future_type) .field("content", &WebMessage::content) .field("time", &WebMessage::time); value_object("Media") .field("id", &Media::id) .field("container", &Media::container) .field("thread", &Media::thread) .field("uri", &Media::uri) .field("type", &Media::type) .field("extras", &Media::extras); value_object("MessageWithMedias") .field("message", &MessageWithMedias::message) .field("medias", &MessageWithMedias::medias); value_object("OlmPersistSession") .field("targetUserID", &OlmPersistSession::target_user_id) .field("sessionData", &OlmPersistSession::session_data); class_("SQLiteQueryExecutor") .constructor() .function("updateDraft", &SQLiteQueryExecutor::updateDraft) .function("moveDraft", &SQLiteQueryExecutor::moveDraft) .function("getAllDrafts", &SQLiteQueryExecutor::getAllDrafts) .function("removeAllDrafts", &SQLiteQueryExecutor::removeAllDrafts) .function("removeDrafts", &SQLiteQueryExecutor::removeDrafts) .function("getAllMessagesWeb", &SQLiteQueryExecutor::getAllMessagesWeb) .function("removeAllMessages", &SQLiteQueryExecutor::removeAllMessages) .function("removeMessages", &SQLiteQueryExecutor::removeMessages) .function( "removeMessagesForThreads", &SQLiteQueryExecutor::removeMessagesForThreads) .function("replaceMessageWeb", &SQLiteQueryExecutor::replaceMessageWeb) .function("rekeyMessage", &SQLiteQueryExecutor::rekeyMessage) .function("removeAllMedia", &SQLiteQueryExecutor::removeAllMedia) .function( "removeMediaForThreads", &SQLiteQueryExecutor::removeMediaForThreads) .function( "removeMediaForMessage", &SQLiteQueryExecutor::removeMediaForMessage) .function( "removeMediaForMessages", &SQLiteQueryExecutor::removeMediaForMessages) .function("replaceMedia", &SQLiteQueryExecutor::replaceMedia) .function( "rekeyMediaContainers", &SQLiteQueryExecutor::rekeyMediaContainers) .function( "replaceMessageStoreThreads", &SQLiteQueryExecutor::replaceMessageStoreThreads) .function( "removeMessageStoreThreads", &SQLiteQueryExecutor::removeMessageStoreThreads) .function( "getAllMessageStoreThreads", &SQLiteQueryExecutor::getAllMessageStoreThreads) .function( "removeAllMessageStoreThreads", &SQLiteQueryExecutor::removeAllMessageStoreThreads) .function("setMetadata", &SQLiteQueryExecutor::setMetadata) .function("clearMetadata", &SQLiteQueryExecutor::clearMetadata) .function("getMetadata", &SQLiteQueryExecutor::getMetadata) .function("replaceReport", &SQLiteQueryExecutor::replaceReport) .function("removeReports", &SQLiteQueryExecutor::removeReports) .function("removeAllReports", &SQLiteQueryExecutor::removeAllReports) .function("getAllReports", &SQLiteQueryExecutor::getAllReports) .function( "setPersistStorageItem", &SQLiteQueryExecutor::setPersistStorageItem) .function( "removePersistStorageItem", &SQLiteQueryExecutor::removePersistStorageItem) .function( "getPersistStorageItem", &SQLiteQueryExecutor::getPersistStorageItem) .function("replaceUser", &SQLiteQueryExecutor::replaceUser) .function("removeUsers", &SQLiteQueryExecutor::removeUsers) .function("removeAllUsers", &SQLiteQueryExecutor::removeAllUsers) .function("getAllUsers", &SQLiteQueryExecutor::getAllUsers) .function("replaceThreadWeb", &SQLiteQueryExecutor::replaceThreadWeb) .function("getAllThreadsWeb", &SQLiteQueryExecutor::getAllThreadsWeb) .function("removeAllThreads", &SQLiteQueryExecutor::removeAllThreads) .function("removeThreads", &SQLiteQueryExecutor::removeThreads) .function("replaceKeyserver", &SQLiteQueryExecutor::replaceKeyserver) .function("removeKeyservers", &SQLiteQueryExecutor::removeKeyservers) .function( "removeAllKeyservers", &SQLiteQueryExecutor::removeAllKeyservers) .function("getAllKeyservers", &SQLiteQueryExecutor::getAllKeyservers) + .function("replaceCommunity", &SQLiteQueryExecutor::replaceCommunity) + .function("removeCommunities", &SQLiteQueryExecutor::removeCommunities) + .function( + "removeAllCommunities", &SQLiteQueryExecutor::removeAllCommunities) + .function("getAllCommunities", &SQLiteQueryExecutor::getAllCommunities) .function("beginTransaction", &SQLiteQueryExecutor::beginTransaction) .function("commitTransaction", &SQLiteQueryExecutor::commitTransaction) .function( "getOlmPersistSessionsData", &SQLiteQueryExecutor::getOlmPersistSessionsData) .function( "getOlmPersistAccountDataWeb", &SQLiteQueryExecutor::getOlmPersistAccountDataWeb) .function( "storeOlmPersistSession", &SQLiteQueryExecutor::storeOlmPersistSession) .function( "storeOlmPersistAccount", &SQLiteQueryExecutor::storeOlmPersistAccount) .function( "rollbackTransaction", &SQLiteQueryExecutor::rollbackTransaction) .function( "restoreFromMainCompaction", &SQLiteQueryExecutor::restoreFromMainCompaction) .function( "restoreFromBackupLog", &SQLiteQueryExecutor::restoreFromBackupLog); } } // namespace comm namespace emscripten { namespace internal { template struct BindingType> { using ValBinding = BindingType; using WireType = ValBinding::WireType; static WireType toWireType(const std::vector &vec) { std::vector valVec(vec.begin(), vec.end()); return BindingType::toWireType(val::array(valVec)); } static std::vector fromWireType(WireType value) { return vecFromJSArray(ValBinding::fromWireType(value)); } }; template struct TypeID< T, typename std::enable_if_t::type, std::vector< typename Canonicalized::type::value_type, typename Canonicalized::type::allocator_type>>::value>> { static constexpr TYPEID get() { return TypeID::get(); } }; } // namespace internal } // namespace emscripten diff --git a/web/database/_generated/comm_query_executor.wasm b/web/database/_generated/comm_query_executor.wasm index 10e67b36a..0a530ec00 100755 Binary files a/web/database/_generated/comm_query_executor.wasm and b/web/database/_generated/comm_query_executor.wasm differ diff --git a/web/database/queries/communities-queries.test.js b/web/database/queries/communities-queries.test.js new file mode 100644 index 000000000..d7671ac73 --- /dev/null +++ b/web/database/queries/communities-queries.test.js @@ -0,0 +1,132 @@ +// @flow + +import { + convertCommunityInfoToClientDBCommunityInfo, + communityStoreOpsHandlers, +} from 'lib/ops/community-store-ops.js'; +import type { CommunityInfo } from 'lib/types/community-types.js'; + +import { getDatabaseModule } from '../db-module.js'; +import type { EmscriptenModule } from '../types/module.js'; +import type { SQLiteQueryExecutor } from '../types/sqlite-query-executor.js'; +import { clearSensitiveData } from '../utils/db-utils.js'; + +const FILE_PATH = 'test.sqlite'; + +const TEST_COMMUNITY_1: CommunityInfo = { + enabledApps: { + calendar: false, + wiki: false, + tasks: true, + files: true, + }, +}; + +const TEST_COMMUNITY_2: CommunityInfo = { + enabledApps: { + calendar: true, + wiki: false, + tasks: false, + files: false, + }, +}; + +describe('Community Store queries', () => { + let queryExecutor: ?SQLiteQueryExecutor = null; + let dbModule: ?EmscriptenModule = null; + + beforeAll(async () => { + dbModule = getDatabaseModule(); + }); + + beforeEach(() => { + if (!dbModule) { + return; + } + + queryExecutor = new dbModule.SQLiteQueryExecutor(FILE_PATH); + queryExecutor?.replaceCommunity( + convertCommunityInfoToClientDBCommunityInfo({ + communityInfo: TEST_COMMUNITY_1, + id: '1', + }), + ); + queryExecutor?.replaceCommunity( + convertCommunityInfoToClientDBCommunityInfo({ + communityInfo: TEST_COMMUNITY_2, + id: '2', + }), + ); + }); + + afterEach(() => { + if (!dbModule || !queryExecutor) { + return; + } + + clearSensitiveData(dbModule, FILE_PATH, queryExecutor); + }); + + it('should return all communities', () => { + const communities = queryExecutor?.getAllCommunities(); + + expect(communities).toHaveLength(2); + }); + + it('should remove all communities', () => { + queryExecutor?.removeAllCommunities(); + const communities = queryExecutor?.getAllCommunities(); + + expect(communities).toHaveLength(0); + }); + + it('should update community enabled apps', () => { + const community2Updated: CommunityInfo = { + enabledApps: { + calendar: true, + wiki: true, + tasks: true, + files: true, + }, + }; + + queryExecutor?.replaceCommunity( + convertCommunityInfoToClientDBCommunityInfo({ + communityInfo: community2Updated, + id: '2', + }), + ); + + const communities = queryExecutor?.getAllCommunities(); + if (!communities) { + throw new Error('communities not defined'); + } + + expect(communities).toHaveLength(2); + + const communitiesFromDB = + communityStoreOpsHandlers.translateClientDBData(communities); + + expect(communitiesFromDB['2']).toBeDefined(); + expect(communitiesFromDB['2'].enabledApps.calendar).toBe(true); + expect(communitiesFromDB['2'].enabledApps.wiki).toBe(true); + expect(communitiesFromDB['2'].enabledApps.tasks).toBe(true); + expect(communitiesFromDB['2'].enabledApps.files).toBe(true); + }); + + it('should remove community', () => { + queryExecutor?.removeCommunities(['2']); + + const communities = queryExecutor?.getAllCommunities(); + if (!communities) { + throw new Error('communities not defined'); + } + + expect(communities.length).toBe(1); + + const communitiesFromDB = + communityStoreOpsHandlers.translateClientDBData(communities); + + expect(communitiesFromDB['1']).toBeDefined(); + }); +}); diff --git a/web/database/types/sqlite-query-executor.js b/web/database/types/sqlite-query-executor.js index 74b045d52..4a76bc798 100644 --- a/web/database/types/sqlite-query-executor.js +++ b/web/database/types/sqlite-query-executor.js @@ -1,123 +1,129 @@ // @flow +import type { ClientDBCommunityInfo } from 'lib/ops/community-store-ops.js'; import type { ClientDBKeyserverInfo } from 'lib/ops/keyserver-store-ops.js'; import type { ClientDBReport } from 'lib/ops/report-store-ops.js'; import type { ClientDBUserInfo } from 'lib/ops/user-store-ops.js'; import type { ClientDBDraftInfo } from 'lib/types/draft-types.js'; import { type WebClientDBThreadInfo, type NullableString, type NullableInt, } from './entities.js'; type WebMessage = { +id: string, +localID: NullableString, +thread: string, +user: string, +type: number, +futureType: NullableInt, +content: NullableString, +time: string, }; type Media = { +id: string, +container: string, +thread: string, +uri: string, +type: string, +extras: string, }; type OlmPersistSession = { +targetUserID: string, +sessionData: string, }; declare export class SQLiteQueryExecutor { constructor(sqliteFilePath: string): void; updateDraft(key: string, text: string): void; moveDraft(oldKey: string, newKey: string): boolean; getAllDrafts(): ClientDBDraftInfo[]; removeAllDrafts(): void; removeDrafts(ids: $ReadOnlyArray): void; getAllMessagesWeb(): $ReadOnlyArray<{ +message: WebMessage, +medias: $ReadOnlyArray, }>; removeAllMessages(): void; removeMessages(ids: $ReadOnlyArray): void; removeMessagesForThreads(threadIDs: $ReadOnlyArray): void; replaceMessageWeb(message: WebMessage): void; rekeyMessage(from: string, to: string): void; removeAllMedia(): void; removeMediaForThreads(threadIDs: $ReadOnlyArray): void; removeMediaForMessages(msgIDs: $ReadOnlyArray): void; removeMediaForMessage(msgID: string): void; replaceMedia(media: Media): void; rekeyMediaContainers(from: string, to: string): void; replaceMessageStoreThreads( threads: $ReadOnlyArray<{ +id: string, +startReached: number }>, ): void; removeMessageStoreThreads($ReadOnlyArray): void; getAllMessageStoreThreads(): $ReadOnlyArray<{ +id: string, +startReached: number, }>; removeAllMessageStoreThreads(): void; setMetadata(entryName: string, data: string): void; clearMetadata(entryName: string): void; getMetadata(entryName: string): string; replaceReport(report: ClientDBReport): void; removeReports(ids: $ReadOnlyArray): void; removeAllReports(): void; getAllReports(): ClientDBReport[]; setPersistStorageItem(key: string, item: string): void; removePersistStorageItem(key: string): void; getPersistStorageItem(key: string): string; replaceUser(userInfo: ClientDBUserInfo): void; removeUsers(ids: $ReadOnlyArray): void; removeAllUsers(): void; getAllUsers(): ClientDBUserInfo[]; replaceThreadWeb(thread: WebClientDBThreadInfo): void; removeThreads(ids: $ReadOnlyArray): void; removeAllThreads(): void; getAllThreadsWeb(): WebClientDBThreadInfo[]; replaceKeyserver(user_info: ClientDBKeyserverInfo): void; removeKeyservers(ids: $ReadOnlyArray): void; removeAllKeyservers(): void; getAllKeyservers(): ClientDBKeyserverInfo[]; + replaceCommunity(communityInfo: ClientDBCommunityInfo): void; + removeCommunities(ids: $ReadOnlyArray): void; + removeAllCommunities(): void; + getAllCommunities(): ClientDBCommunityInfo[]; + beginTransaction(): void; commitTransaction(): void; rollbackTransaction(): void; getOlmPersistAccountDataWeb(): NullableString; getOlmPersistSessionsData(): $ReadOnlyArray; storeOlmPersistAccount(accountData: string): void; storeOlmPersistSession(session: OlmPersistSession): void; restoreFromMainCompaction( mainCompactionPath: string, mainCompactionEncryptionKey: string, ): void; restoreFromBackupLog(backupLog: Uint8Array): void; // method is provided to manually signal that a C++ object // is no longer needed and can be deleted delete(): void; } export type SQLiteQueryExecutorType = typeof SQLiteQueryExecutor;