From 910191b07443dc4ea42d90f466e9f4ca8bb8c596 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 21 Oct 2022 15:58:23 +0100 Subject: [PATCH] Added optional chatReference field to CHAT transactions. This allows one message to reference another, e.g. for replies, edits, and reactions. We can't use the existing reference field as this is used for encryption and generally points to the user's lastReference at the time of signing. "chatReference" is based on the "nameReference" field used in various name transactions, for similar purposes. This needs a feature trigger timestamp to activate, and that same timestamp will need to be used in the UI since that is responsible for building the chat transactions. --- .../org/qortal/api/resource/ChatResource.java | 6 ++++ .../api/websocket/ChatMessagesWebSocket.java | 2 ++ .../java/org/qortal/block/BlockChain.java | 7 ++++- .../data/transaction/ChatTransactionData.java | 9 +++++- .../org/qortal/repository/ChatRepository.java | 2 +- .../hsqldb/HSQLDBChatRepository.java | 7 ++++- .../hsqldb/HSQLDBDatabaseUpdates.java | 6 ++++ .../HSQLDBChatTransactionRepository.java | 5 +-- .../ChatTransactionTransformer.java | 31 +++++++++++++++++-- src/main/resources/blockchain.json | 3 +- .../test-chain-v2-block-timestamps.json | 3 +- .../test-chain-v2-disable-reference.json | 3 +- .../test-chain-v2-founder-rewards.json | 3 +- .../test-chain-v2-leftover-reward.json | 3 +- src/test/resources/test-chain-v2-minting.json | 3 +- .../test-chain-v2-qora-holder-extremes.json | 3 +- .../test-chain-v2-qora-holder-reduction.json | 3 +- .../resources/test-chain-v2-qora-holder.json | 3 +- .../test-chain-v2-reward-levels.json | 3 +- .../test-chain-v2-reward-scaling.json | 3 +- .../test-chain-v2-reward-shares.json | 3 +- src/test/resources/test-chain-v2.json | 3 +- 22 files changed, 93 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/ChatResource.java b/src/main/java/org/qortal/api/resource/ChatResource.java index ee2a8599..8c0f94c3 100644 --- a/src/main/java/org/qortal/api/resource/ChatResource.java +++ b/src/main/java/org/qortal/api/resource/ChatResource.java @@ -70,6 +70,7 @@ public class ChatResource { @QueryParam("txGroupId") Integer txGroupId, @QueryParam("involving") List involvingAddresses, @QueryParam("reference") String reference, + @QueryParam("chatreference") String chatReference, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) { @@ -92,12 +93,17 @@ public class ChatResource { if (reference != null) referenceBytes = Base58.decode(reference); + byte[] chatReferenceBytes = null; + if (chatReference != null) + chatReferenceBytes = Base58.decode(chatReference); + try (final Repository repository = RepositoryManager.getRepository()) { return repository.getChatRepository().getMessagesMatchingCriteria( before, after, txGroupId, referenceBytes, + chatReferenceBytes, involvingAddresses, limit, offset, reverse); } catch (DataException e) { diff --git a/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java b/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java index 9760b7f0..dbe36d9f 100644 --- a/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java +++ b/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java @@ -47,6 +47,7 @@ public class ChatMessagesWebSocket extends ApiWebSocket { txGroupId, null, null, + null, null, null, null); sendMessages(session, chatMessages); @@ -74,6 +75,7 @@ public class ChatMessagesWebSocket extends ApiWebSocket { null, null, null, + null, involvingAddresses, null, null, null); diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 42692a18..d483f8d7 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -73,7 +73,8 @@ public class BlockChain { calcChainWeightTimestamp, transactionV5Timestamp, transactionV6Timestamp, - disableReferenceTimestamp; + disableReferenceTimestamp, + chatReferenceTimestamp; } // Custom transaction fees @@ -486,6 +487,10 @@ public class BlockChain { return this.featureTriggers.get(FeatureTrigger.disableReferenceTimestamp.name()).longValue(); } + public long getChatReferenceTimestamp() { + return this.featureTriggers.get(FeatureTrigger.chatReferenceTimestamp.name()).longValue(); + } + // More complex getters for aspects that change by height or timestamp diff --git a/src/main/java/org/qortal/data/transaction/ChatTransactionData.java b/src/main/java/org/qortal/data/transaction/ChatTransactionData.java index 36ce6124..81bdb2b7 100644 --- a/src/main/java/org/qortal/data/transaction/ChatTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/ChatTransactionData.java @@ -26,6 +26,8 @@ public class ChatTransactionData extends TransactionData { private String recipient; // can be null + private byte[] chatReference; // can be null + @Schema(description = "raw message data, possibly UTF8 text", example = "2yGEbwRFyhPZZckKA") private byte[] data; @@ -44,13 +46,14 @@ public class ChatTransactionData extends TransactionData { } public ChatTransactionData(BaseTransactionData baseTransactionData, - String sender, int nonce, String recipient, byte[] data, boolean isText, boolean isEncrypted) { + String sender, int nonce, String recipient, byte[] chatReference, byte[] data, boolean isText, boolean isEncrypted) { super(TransactionType.CHAT, baseTransactionData); this.senderPublicKey = baseTransactionData.creatorPublicKey; this.sender = sender; this.nonce = nonce; this.recipient = recipient; + this.chatReference = chatReference; this.data = data; this.isText = isText; this.isEncrypted = isEncrypted; @@ -78,6 +81,10 @@ public class ChatTransactionData extends TransactionData { return this.recipient; } + public byte[] getChatReference() { + return this.chatReference; + } + public byte[] getData() { return this.data; } diff --git a/src/main/java/org/qortal/repository/ChatRepository.java b/src/main/java/org/qortal/repository/ChatRepository.java index 2ecd8a34..ebdc22e4 100644 --- a/src/main/java/org/qortal/repository/ChatRepository.java +++ b/src/main/java/org/qortal/repository/ChatRepository.java @@ -14,7 +14,7 @@ public interface ChatRepository { * Expects EITHER non-null txGroupID OR non-null sender and recipient addresses. */ public List getMessagesMatchingCriteria(Long before, Long after, - Integer txGroupId, byte[] reference, List involving, + Integer txGroupId, byte[] reference, byte[] chatReferenceBytes, List involving, Integer limit, Integer offset, Boolean reverse) throws DataException; public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java index 2f570686..d4c9d7e0 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java @@ -24,7 +24,7 @@ public class HSQLDBChatRepository implements ChatRepository { @Override public List getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId, byte[] referenceBytes, - List involving, Integer limit, Integer offset, Boolean reverse) + byte[] chatReferenceBytes, List involving, Integer limit, Integer offset, Boolean reverse) throws DataException { // Check args meet expectations if ((txGroupId != null && involving != null && !involving.isEmpty()) @@ -62,6 +62,11 @@ public class HSQLDBChatRepository implements ChatRepository { bindParams.add(referenceBytes); } + if (chatReferenceBytes != null) { + whereClauses.add("chat_reference = ?"); + bindParams.add(chatReferenceBytes); + } + if (txGroupId != null) { whereClauses.add("tx_group_id = " + txGroupId); // int safe to use literally whereClauses.add("recipient IS NULL"); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 1174f5c8..53458484 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -975,6 +975,12 @@ public class HSQLDBDatabaseUpdates { stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN receiving_account_info SET DATA TYPE VARBINARY(128)"); break; + case 44: + // Add a chat reference, to allow one message to reference another, and for this to be easily + // searchable. Null values are allowed as most transactions won't have a reference. + stmt.execute("ALTER TABLE ChatTransactions ADD chat_reference Signature"); + break; + default: // nothing to do return false; diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBChatTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBChatTransactionRepository.java index 449922f4..0dd3c0e3 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBChatTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBChatTransactionRepository.java @@ -17,7 +17,7 @@ public class HSQLDBChatTransactionRepository extends HSQLDBTransactionRepository } TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT sender, nonce, recipient, is_text, is_encrypted, data FROM ChatTransactions WHERE signature = ?"; + String sql = "SELECT sender, nonce, recipient, is_text, is_encrypted, data, chat_reference FROM ChatTransactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { if (resultSet == null) @@ -29,8 +29,9 @@ public class HSQLDBChatTransactionRepository extends HSQLDBTransactionRepository boolean isText = resultSet.getBoolean(4); boolean isEncrypted = resultSet.getBoolean(5); byte[] data = resultSet.getBytes(6); + byte[] chatReference = resultSet.getBytes(7); - return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, data, isText, isEncrypted); + return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, chatReference, data, isText, isEncrypted); } catch (SQLException e) { throw new DataException("Unable to fetch chat transaction from repository", e); } diff --git a/src/main/java/org/qortal/transform/transaction/ChatTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/ChatTransactionTransformer.java index 69a9ef5b..d482dacd 100644 --- a/src/main/java/org/qortal/transform/transaction/ChatTransactionTransformer.java +++ b/src/main/java/org/qortal/transform/transaction/ChatTransactionTransformer.java @@ -4,6 +4,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; +import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.ChatTransactionData; @@ -22,11 +23,13 @@ public class ChatTransactionTransformer extends TransactionTransformer { private static final int NONCE_LENGTH = INT_LENGTH; private static final int HAS_RECIPIENT_LENGTH = BOOLEAN_LENGTH; private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH; + private static final int HAS_CHAT_REFERENCE_LENGTH = BOOLEAN_LENGTH; + private static final int CHAT_REFERENCE_LENGTH = SIGNATURE_LENGTH; private static final int DATA_SIZE_LENGTH = INT_LENGTH; private static final int IS_TEXT_LENGTH = BOOLEAN_LENGTH; private static final int IS_ENCRYPTED_LENGTH = BOOLEAN_LENGTH; - private static final int EXTRAS_LENGTH = NONCE_LENGTH + HAS_RECIPIENT_LENGTH + DATA_SIZE_LENGTH + IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH; + private static final int EXTRAS_LENGTH = NONCE_LENGTH + HAS_RECIPIENT_LENGTH + HAS_CHAT_REFERENCE_LENGTH + DATA_SIZE_LENGTH + IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH; protected static final TransactionLayout layout; @@ -63,6 +66,17 @@ public class ChatTransactionTransformer extends TransactionTransformer { boolean hasRecipient = byteBuffer.get() != 0; String recipient = hasRecipient ? Serialization.deserializeAddress(byteBuffer) : null; + byte[] chatReference = null; + + if (timestamp >= BlockChain.getInstance().getChatReferenceTimestamp()) { + boolean hasChatReference = byteBuffer.get() != 0; + + if (hasChatReference) { + chatReference = new byte[CHAT_REFERENCE_LENGTH]; + byteBuffer.get(chatReference); + } + } + int dataSize = byteBuffer.getInt(); // Don't allow invalid dataSize here to avoid run-time issues if (dataSize > ChatTransaction.MAX_DATA_SIZE) @@ -83,7 +97,7 @@ public class ChatTransactionTransformer extends TransactionTransformer { BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature); String sender = Crypto.toAddress(senderPublicKey); - return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, data, isText, isEncrypted); + return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, chatReference, data, isText, isEncrypted); } public static int getDataLength(TransactionData transactionData) { @@ -94,6 +108,9 @@ public class ChatTransactionTransformer extends TransactionTransformer { if (chatTransactionData.getRecipient() != null) dataLength += RECIPIENT_LENGTH; + if (chatTransactionData.getChatReference() != null) + dataLength += CHAT_REFERENCE_LENGTH; + return dataLength; } @@ -114,6 +131,16 @@ public class ChatTransactionTransformer extends TransactionTransformer { bytes.write((byte) 0); } + if (transactionData.getTimestamp() >= BlockChain.getInstance().getChatReferenceTimestamp()) { + // Include chat reference if it's not null + if (chatTransactionData.getChatReference() != null) { + bytes.write((byte) 1); + bytes.write(chatTransactionData.getChatReference()); + } else { + bytes.write((byte) 0); + } + } + bytes.write(Ints.toByteArray(chatTransactionData.getData().length)); bytes.write(chatTransactionData.getData()); diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index e9f1500d..2255c0a8 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -80,7 +80,8 @@ "calcChainWeightTimestamp": 1620579600000, "transactionV5Timestamp": 1642176000000, "transactionV6Timestamp": 9999999999999, - "disableReferenceTimestamp": 1655222400000 + "disableReferenceTimestamp": 1655222400000, + "chatReferenceTimestamp": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 37224684..028519f8 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -69,7 +69,8 @@ "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 9999999999999, - "disableReferenceTimestamp": 9999999999999 + "disableReferenceTimestamp": 9999999999999, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index 7ea0b86d..541ce779 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -72,7 +72,8 @@ "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, - "disableReferenceTimestamp": 0 + "disableReferenceTimestamp": 0, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 85a50f83..7392d5ae 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -73,7 +73,8 @@ "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, - "disableReferenceTimestamp": 9999999999999 + "disableReferenceTimestamp": 9999999999999, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index ebc3ccfa..ed46cd56 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -73,7 +73,8 @@ "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, - "disableReferenceTimestamp": 9999999999999 + "disableReferenceTimestamp": 9999999999999, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index cc91f993..a705016c 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -73,7 +73,8 @@ "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, - "disableReferenceTimestamp": 9999999999999 + "disableReferenceTimestamp": 9999999999999, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 085d1dbf..880af61b 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -73,7 +73,8 @@ "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, - "disableReferenceTimestamp": 9999999999999 + "disableReferenceTimestamp": 9999999999999, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder-reduction.json b/src/test/resources/test-chain-v2-qora-holder-reduction.json index 75858057..27451201 100644 --- a/src/test/resources/test-chain-v2-qora-holder-reduction.json +++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json @@ -74,7 +74,8 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "aggregateSignatureTimestamp": 0 + "aggregateSignatureTimestamp": 0, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 0706c5bb..fbc58d80 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -73,7 +73,8 @@ "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, - "disableReferenceTimestamp": 9999999999999 + "disableReferenceTimestamp": 9999999999999, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index b3644d6b..0f7adf6f 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -73,7 +73,8 @@ "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, - "disableReferenceTimestamp": 9999999999999 + "disableReferenceTimestamp": 9999999999999, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index 1c68dda4..802ad8fe 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -73,7 +73,8 @@ "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, - "disableReferenceTimestamp": 9999999999999 + "disableReferenceTimestamp": 9999999999999, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index 10d2aab3..2becc875 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -73,7 +73,8 @@ "newConsensusTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, - "disableReferenceTimestamp": 9999999999999 + "disableReferenceTimestamp": 9999999999999, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 5f439602..c3b740ff 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -73,7 +73,8 @@ "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, - "disableReferenceTimestamp": 9999999999999 + "disableReferenceTimestamp": 9999999999999, + "chatReferenceTimestamp": 0 }, "genesisInfo": { "version": 4,