From 32470fa6416b7f72d7e784eb25029dc608e57309 Mon Sep 17 00:00:00 2001 From: catbref Date: Tue, 12 May 2020 10:02:41 +0100 Subject: [PATCH] Improve CHAT API and repository support. Change CHAT API call GET /chat/search to better support the two main scenarios of: group-based chatting: supply txGroupId only private chatting: supply 2 'involving' addresses only Added some DB indexes to cater for above. GET /chat/search now returns specialized ChatMessage objects instead of ChatTransactions. This is to reduce unnecessary fetching of data from repository, and onward sending to API client. --- .../org/qortal/api/resource/ChatResource.java | 27 +++--- .../org/qortal/data/chat/ChatMessage.java | 95 +++++++++++++++++++ .../org/qortal/repository/ChatRepository.java | 15 +-- .../hsqldb/HSQLDBChatRepository.java | 87 ++++++++--------- .../hsqldb/HSQLDBDatabaseUpdates.java | 4 + 5 files changed, 165 insertions(+), 63 deletions(-) create mode 100644 src/main/java/org/qortal/data/chat/ChatMessage.java diff --git a/src/main/java/org/qortal/api/resource/ChatResource.java b/src/main/java/org/qortal/api/resource/ChatResource.java index 3d9a54af..82dc2bec 100644 --- a/src/main/java/org/qortal/api/resource/ChatResource.java +++ b/src/main/java/org/qortal/api/resource/ChatResource.java @@ -24,6 +24,7 @@ import org.qortal.api.ApiErrors; import org.qortal.api.ApiExceptionFactory; import org.qortal.api.Security; import org.qortal.crypto.Crypto; +import org.qortal.data.chat.ChatMessage; import org.qortal.data.transaction.ChatTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.repository.DataException; @@ -51,14 +52,14 @@ public class ChatResource { @Path("/search") @Operation( summary = "Find chat messages", - description = "Returns CHAT transactions that match criteria.", + description = "Returns CHAT messages that match criteria. Must provide EITHER 'txGroupId' OR two 'involving' addresses.", responses = { @ApiResponse( - description = "transactions", + description = "CHAT messages", content = @Content( array = @ArraySchema( schema = @Schema( - implementation = ChatTransactionData.class + implementation = ChatMessage.class ) ) ) @@ -66,18 +67,19 @@ public class ChatResource { } ) @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) - public List searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after, + public List searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after, @QueryParam("txGroupId") Integer txGroupId, - @QueryParam("sender") String senderAddress, - @QueryParam("recipient") String recipientAddress, + @QueryParam("involving") List involvingAddresses, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) { - // Check any provided addresses are valid - if (senderAddress != null && !Crypto.isValidAddress(senderAddress)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + // Check args meet expectations + if ((txGroupId == null && involvingAddresses.size() != 2) + || (txGroupId != null && !involvingAddresses.isEmpty())) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - if (recipientAddress != null && !Crypto.isValidAddress(recipientAddress)) + // Check any provided addresses are valid + if (involvingAddresses.stream().anyMatch(address -> !Crypto.isValidAddress(address))) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); if (before != null && before < 1500000000000L) @@ -87,12 +89,11 @@ public class ChatResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); try (final Repository repository = RepositoryManager.getRepository()) { - return repository.getChatRepository().getTransactionsMatchingCriteria( + return repository.getChatRepository().getMessagesMatchingCriteria( before, after, txGroupId, - senderAddress, - recipientAddress, + involvingAddresses, limit, offset, reverse); } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); diff --git a/src/main/java/org/qortal/data/chat/ChatMessage.java b/src/main/java/org/qortal/data/chat/ChatMessage.java new file mode 100644 index 00000000..42ad57e9 --- /dev/null +++ b/src/main/java/org/qortal/data/chat/ChatMessage.java @@ -0,0 +1,95 @@ +package org.qortal.data.chat; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +@XmlAccessorType(XmlAccessType.FIELD) +public class ChatMessage { + + // Properties + + private long timestamp; + + private int txGroupId; + + private byte[] senderPublicKey; + + /* Address of sender */ + private String sender; + + /* Registered name of sender (if any) */ + private String senderName; // can be null + + /* Address of recipient (if any) */ + private String recipient; // can be null + + /* Registered name of recipient (if any) */ + private String recipientName; // can be null + + private byte[] data; + + private boolean isText; + private boolean isEncrypted; + + // Constructors + + protected ChatMessage() { + /* For JAXB */ + } + + // For repository use + public ChatMessage(long timestamp, int txGroupId, byte[] senderPublicKey, String sender, String senderName, + String recipient, String recipientName, byte[] data, boolean isText, boolean isEncrypted) { + this.timestamp = timestamp; + this.txGroupId = txGroupId; + this.senderPublicKey = senderPublicKey; + this.sender = sender; + this.senderName = senderName; + this.recipient = recipient; + this.recipientName = recipientName; + this.data = data; + this.isText = isText; + this.isEncrypted = isEncrypted; + } + + public long getTimestamp() { + return this.timestamp; + } + + public int getTxGroupId() { + return this.txGroupId; + } + + public byte[] getSenderPublicKey() { + return this.senderPublicKey; + } + + public String getSender() { + return this.sender; + } + + public String getSenderName() { + return this.senderName; + } + + public String getRecipient() { + return this.recipient; + } + + public String getRecipientName() { + return this.recipientName; + } + + public byte[] getData() { + return this.data; + } + + public boolean isText() { + return this.isText; + } + + public boolean isEncrypted() { + return this.isEncrypted; + } + +} diff --git a/src/main/java/org/qortal/repository/ChatRepository.java b/src/main/java/org/qortal/repository/ChatRepository.java index 9efbb56c..504de8cb 100644 --- a/src/main/java/org/qortal/repository/ChatRepository.java +++ b/src/main/java/org/qortal/repository/ChatRepository.java @@ -2,16 +2,17 @@ package org.qortal.repository; import java.util.List; -import org.qortal.data.transaction.ChatTransactionData; +import org.qortal.data.chat.ChatMessage; public interface ChatRepository { - public List getTransactionsMatchingCriteria( - Long before, - Long after, - Integer txGroupId, - String senderAddress, - String recipientAddress, + /** + * Returns CHAT messages matching criteria. + *

+ * Expects EITHER non-null txGroupID OR non-null sender and recipient addresses. + */ + public List getMessagesMatchingCriteria(Long before, Long after, + Integer txGroupId, List involving, Integer limit, Integer offset, Boolean reverse) 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 7ff7df21..08d141b0 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java @@ -5,10 +5,9 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; -import org.qortal.data.transaction.ChatTransactionData; +import org.qortal.data.chat.ChatMessage; import org.qortal.repository.ChatRepository; import org.qortal.repository.DataException; -import org.qortal.transaction.Transaction.TransactionType; public class HSQLDBChatRepository implements ChatRepository { @@ -19,58 +18,48 @@ public class HSQLDBChatRepository implements ChatRepository { } @Override - public List getTransactionsMatchingCriteria(Long before, Long after, Integer txGroupId, - String senderAddress, String recipientAddress, Integer limit, Integer offset, Boolean reverse) + public List getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId, + List involving, Integer limit, Integer offset, Boolean reverse) throws DataException { - boolean hasSenderAddress = senderAddress != null && !senderAddress.isEmpty(); - boolean hasRecipientAddress = recipientAddress != null && !recipientAddress.isEmpty(); + // Check args meet expectations + if ((txGroupId != null && involving != null && !involving.isEmpty()) + || (txGroupId == null && (involving == null || involving.size() != 2))) + throw new DataException("Invalid criteria for fetching chat messages from repository"); + + StringBuilder sql = new StringBuilder(1024); + + sql.append("SELECT created_when, tx_group_id, creator, sender, SenderNames.name, " + + "recipient, RecipientNames.name, data, is_text, is_encrypted " + + "FROM ChatTransactions " + + "JOIN Transactions USING (signature) " + + "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender " + + "LEFT OUTER JOIN Names AS RecipientNames ON RecipientNames.owner = recipient "); + + // WHERE clauses - String signatureColumn = "Transactions.signature"; List whereClauses = new ArrayList<>(); List bindParams = new ArrayList<>(); - // Tables, starting with Transactions - StringBuilder tables = new StringBuilder(256); - tables.append("Transactions"); - - if (hasSenderAddress || hasRecipientAddress) - tables.append(" JOIN ChatTransactions USING (signature)"); - - // WHERE clauses next - - // CHAT transaction type - whereClauses.add("Transactions.type = " + TransactionType.CHAT.value); - // Timestamp range if (before != null) { - whereClauses.add("Transactions.created_when < ?"); + whereClauses.add("created_when < ?"); bindParams.add(before); } if (after != null) { - whereClauses.add("Transactions.created_when > ?"); + whereClauses.add("created_when > ?"); bindParams.add(after); } - if (txGroupId != null) - whereClauses.add("Transactions.tx_group_id = " + txGroupId); - - if (hasSenderAddress) { - whereClauses.add("ChatTransactions.sender = ?"); - bindParams.add(senderAddress); + if (txGroupId != null) { + whereClauses.add("tx_group_id = " + txGroupId); // int safe to use literally + whereClauses.add("recipient IS NULL"); + } else { + whereClauses.add("((sender = ? AND recipient = ?) OR (recipient = ? AND sender = ?))"); + bindParams.addAll(involving); + bindParams.addAll(involving); } - if (hasRecipientAddress) { - whereClauses.add("ChatTransactions.recipient = ?"); - bindParams.add(recipientAddress); - } - - StringBuilder sql = new StringBuilder(1024); - sql.append("SELECT "); - sql.append(signatureColumn); - sql.append(" FROM "); - sql.append(tables); - if (!whereClauses.isEmpty()) { sql.append(" WHERE "); @@ -88,19 +77,31 @@ public class HSQLDBChatRepository implements ChatRepository { HSQLDBRepository.limitOffsetSql(sql, limit, offset); - List chatTransactionsData = new ArrayList<>(); + List chatMessages = new ArrayList<>(); try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) { if (resultSet == null) - return chatTransactionsData; + return chatMessages; do { - byte[] signature = resultSet.getBytes(1); + long timestamp = resultSet.getLong(1); + int groupId = resultSet.getInt(2); + byte[] senderPublicKey = resultSet.getBytes(3); + String sender = resultSet.getString(4); + String senderName = resultSet.getString(5); + String recipient = resultSet.getString(6); + String recipientName = resultSet.getString(7); + byte[] data = resultSet.getBytes(8); + boolean isText = resultSet.getBoolean(9); + boolean isEncrypted = resultSet.getBoolean(10); - chatTransactionsData.add((ChatTransactionData) this.repository.getTransactionRepository().fromSignature(signature)); + ChatMessage chatMessage = new ChatMessage(timestamp, groupId, senderPublicKey, sender, + senderName, recipient, recipientName, data, isText, isEncrypted); + + chatMessages.add(chatMessage); } while (resultSet.next()); - return chatTransactionsData; + return chatMessages; } catch (SQLException e) { throw new DataException("Unable to fetch matching chat transactions from repository", e); } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index f3e940f6..730740cd 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -598,6 +598,10 @@ public class HSQLDBDatabaseUpdates { // Chat transactions stmt.execute("CREATE TABLE ChatTransactions (signature Signature, sender QortalAddress NOT NULL, nonce INT NOT NULL, recipient QortalAddress, " + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, data MessageData NOT NULL, " + TRANSACTION_KEYS + ")"); + // For finding chat messages by sender + stmt.execute("CREATE INDEX ChatTransactionsSenderIndex ON ChatTransactions (sender)"); + // For finding chat messages by recipient + stmt.execute("CREATE INDEX ChatTransactionsRecipientIndex ON ChatTransactions (recipient, sender)"); break; default: