forked from Qortal/qortal
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:
parent
0d1c08bf96
commit
32470fa641
@ -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);
|
||||
|
95
src/main/java/org/qortal/data/chat/ChatMessage.java
Normal file
95
src/main/java/org/qortal/data/chat/ChatMessage.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user