diff --git a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h --- a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h @@ -166,6 +166,8 @@ virtual void markOutboundP2PMessageAsSent( std::string messageID, std::string deviceID) const = 0; + virtual std::vector + resetOutboundP2PMessagesForDevice(std::string deviceID) const = 0; virtual void addInboundP2PMessage(InboundP2PMessage message) const = 0; virtual std::vector getAllInboundP2PMessage() const = 0; virtual void diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h @@ -180,6 +180,8 @@ std::string ciphertext) const override; void markOutboundP2PMessageAsSent(std::string messageID, std::string deviceID) const override; + std::vector + resetOutboundP2PMessagesForDevice(std::string deviceID) const override; void addInboundP2PMessage(InboundP2PMessage message) const override; std::vector getAllInboundP2PMessage() const override; void diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp @@ -9,6 +9,7 @@ #include "entities/KeyserverInfo.h" #include "entities/LocalMessageInfo.h" #include "entities/Metadata.h" +#include "entities/SQLiteDataConverters.h" #include "entities/SyncedMetadataEntry.h" #include "entities/UserInfo.h" #include @@ -2543,6 +2544,83 @@ sqlite3_step(preparedSQL); } +std::vector SQLiteQueryExecutor::resetOutboundP2PMessagesForDevice( + std::string deviceID) const { + // Query all messages that need to be resent - all message that supports + // auto retry or already sent messages. + std::string queryMessageIDsToResend = + "SELECT message_id " + "FROM outbound_p2p_messages " + "WHERE device_id = ? AND ( " + " supports_auto_retry = 1 " + " OR (supports_auto_retry = 0 AND status = 'sent') " + ");"; + + SQLiteStatementWrapper preparedQueryMessageIDsSQL( + SQLiteQueryExecutor::getConnection(), + queryMessageIDsToResend, + "Failed to get all messages to reset"); + + bindStringToSQL(deviceID.c_str(), preparedQueryMessageIDsSQL, 1); + std::vector messageIDs; + for (int stepResult = sqlite3_step(preparedQueryMessageIDsSQL); + stepResult == SQLITE_ROW; + stepResult = sqlite3_step(preparedQueryMessageIDsSQL)) { + messageIDs.push_back(getStringFromSQLRow(preparedQueryMessageIDsSQL, 0)); + } + + // Setting ciphertext to an empty string to make sure this message will be + // encrypted again with a new session, update the status, and set + // supports_auto_retry to true. + // Updating supports_auto_retry to true because those are already sent + // messages (from the UI perspective), but the recipient failed to decrypt + // so needs to be automatically resent. + std::stringstream resetMessagesSQLStream; + resetMessagesSQLStream + << "UPDATE outbound_p2p_messages " + << "SET supports_auto_retry = 1, status = 'persisted', ciphertext = '' " + << "WHERE message_id IN " << getSQLStatementArray(messageIDs.size()) + << ";"; + + SQLiteStatementWrapper preparedUpdateSQL( + SQLiteQueryExecutor::getConnection(), + resetMessagesSQLStream.str(), + "Failed to reset messages."); + + for (int i = 0; i < messageIDs.size(); i++) { + int bindResult = bindStringToSQL(messageIDs[i], preparedUpdateSQL, i + 1); + if (bindResult != SQLITE_OK) { + std::stringstream error_message; + error_message << "Failed to bind key to SQL statement. Details: " + << sqlite3_errstr(bindResult) << std::endl; + sqlite3_finalize(preparedUpdateSQL); + throw std::runtime_error(error_message.str()); + } + } + sqlite3_step(preparedUpdateSQL); + + // This handles the case of messages that are encrypted (with a malformed + // session) but not yet queued on Tunnelbroker. In this case, this message + // is not considered to be sent (from the UI perspective), + // and supports_auto_retry is not updated. + std::string updateCiphertextQuery = + "UPDATE outbound_p2p_messages " + "SET ciphertext = '', status = 'persisted'" + "WHERE device_id = ? " + " AND supports_auto_retry = 0 " + " AND status = 'encrypted';"; + + SQLiteStatementWrapper preparedUpdateCiphertextSQL( + SQLiteQueryExecutor::getConnection(), + updateCiphertextQuery, + "Failed to set ciphertext"); + + bindStringToSQL(deviceID.c_str(), preparedUpdateCiphertextSQL, 1); + sqlite3_step(preparedUpdateCiphertextSQL); + + return messageIDs; +} + void SQLiteQueryExecutor::addInboundP2PMessage( InboundP2PMessage message) const { static std::string addMessage = diff --git a/web/cpp/SQLiteQueryExecutorBindings.cpp b/web/cpp/SQLiteQueryExecutorBindings.cpp --- a/web/cpp/SQLiteQueryExecutorBindings.cpp +++ b/web/cpp/SQLiteQueryExecutorBindings.cpp @@ -307,6 +307,9 @@ .function( "markOutboundP2PMessageAsSent", &SQLiteQueryExecutor::markOutboundP2PMessageAsSent) + .function( + "resetOutboundP2PMessagesForDevice", + &SQLiteQueryExecutor::resetOutboundP2PMessagesForDevice) .function( "addInboundP2PMessage", &SQLiteQueryExecutor::addInboundP2PMessage) .function( diff --git a/web/shared-worker/_generated/comm_query_executor.wasm b/web/shared-worker/_generated/comm_query_executor.wasm index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ { expect(queryExecutor?.getOutboundP2PMessagesByID(['id-5'])).toEqual([]); }); + + it('should reset messages', () => { + const deviceID = 'deviceID'; + const MSG_TO_RESET_1: OutboundP2PMessage = { + messageID: 'reset-1', + deviceID, + userID: 'user-1', + timestamp: '1', + plaintext: 'decrypted-1', + ciphertext: 'encrypted-1', + status: 'encrypted', + supportsAutoRetry: true, + }; + + const MSG_TO_RESET_2: OutboundP2PMessage = { + messageID: 'reset-2', + deviceID, + userID: 'user-1', + timestamp: '1', + plaintext: 'decrypted-1', + ciphertext: 'encrypted-1', + status: 'sent', + supportsAutoRetry: false, + }; + + const MSG_NOT_RESET: OutboundP2PMessage = { + messageID: 'reset-3', + deviceID, + userID: 'user-1', + timestamp: '3', + plaintext: 'decrypted-1', + ciphertext: 'encrypted-1', + status: 'encrypted', + supportsAutoRetry: false, + }; + + queryExecutor?.addOutboundP2PMessages([ + MSG_TO_RESET_1, + MSG_TO_RESET_2, + MSG_NOT_RESET, + ]); + + const messageIDs = + queryExecutor?.resetOutboundP2PMessagesForDevice(deviceID); + + expect(messageIDs).toEqual([ + MSG_TO_RESET_1.messageID, + MSG_TO_RESET_2.messageID, + ]); + + const messagesAfterReset = [ + { + ...MSG_TO_RESET_1, + status: outboundP2PMessageStatuses.persisted, + ciphertext: '', + supportsAutoRetry: true, + }, + { + ...MSG_TO_RESET_2, + status: outboundP2PMessageStatuses.persisted, + ciphertext: '', + supportsAutoRetry: true, + }, + ]; + expect(queryExecutor?.getOutboundP2PMessagesByID(messageIDs ?? [])).toEqual( + messagesAfterReset, + ); + expect( + queryExecutor?.getOutboundP2PMessagesByID([MSG_NOT_RESET.messageID]), + ).toEqual([ + { + ...MSG_NOT_RESET, + ciphertext: '', + status: outboundP2PMessageStatuses.persisted, + }, + ]); + }); }); diff --git a/web/shared-worker/types/sqlite-query-executor.js b/web/shared-worker/types/sqlite-query-executor.js --- a/web/shared-worker/types/sqlite-query-executor.js +++ b/web/shared-worker/types/sqlite-query-executor.js @@ -190,6 +190,7 @@ ciphertext: string, ): void; markOutboundP2PMessageAsSent(messageID: string, deviceID: string): void; + resetOutboundP2PMessagesForDevice(deviceID: string): $ReadOnlyArray; addInboundP2PMessage(message: InboundP2PMessage): void; getAllInboundP2PMessage(): $ReadOnlyArray;