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.
This commit is contained in:
catbref 2020-05-12 10:02:41 +01:00
parent 0d1c08bf96
commit 32470fa641
5 changed files with 165 additions and 63 deletions

View File

@ -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<ChatTransactionData> searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after,
public List<ChatMessage> searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after,
@QueryParam("txGroupId") Integer txGroupId,
@QueryParam("sender") String senderAddress,
@QueryParam("recipient") String recipientAddress,
@QueryParam("involving") List<String> 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);

View File

@ -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;
}
}

View File

@ -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<ChatTransactionData> getTransactionsMatchingCriteria(
Long before,
Long after,
Integer txGroupId,
String senderAddress,
String recipientAddress,
/**
* Returns CHAT messages matching criteria.
* <p>
* Expects EITHER non-null txGroupID OR non-null sender and recipient addresses.
*/
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after,
Integer txGroupId, List<String> involving,
Integer limit, Integer offset, Boolean reverse) throws DataException;
}

View File

@ -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<ChatTransactionData> getTransactionsMatchingCriteria(Long before, Long after, Integer txGroupId,
String senderAddress, String recipientAddress, Integer limit, Integer offset, Boolean reverse)
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId,
List<String> 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<String> whereClauses = new ArrayList<>();
List<Object> 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<ChatTransactionData> chatTransactionsData = new ArrayList<>();
List<ChatMessage> 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);
}

View File

@ -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: