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.ApiExceptionFactory;
|
||||||
import org.qortal.api.Security;
|
import org.qortal.api.Security;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.data.chat.ChatMessage;
|
||||||
import org.qortal.data.transaction.ChatTransactionData;
|
import org.qortal.data.transaction.ChatTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
@ -51,14 +52,14 @@ public class ChatResource {
|
|||||||
@Path("/search")
|
@Path("/search")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Find chat messages",
|
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 = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
description = "transactions",
|
description = "CHAT messages",
|
||||||
content = @Content(
|
content = @Content(
|
||||||
array = @ArraySchema(
|
array = @ArraySchema(
|
||||||
schema = @Schema(
|
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})
|
@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("txGroupId") Integer txGroupId,
|
||||||
@QueryParam("sender") String senderAddress,
|
@QueryParam("involving") List<String> involvingAddresses,
|
||||||
@QueryParam("recipient") String recipientAddress,
|
|
||||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||||
// Check any provided addresses are valid
|
// Check args meet expectations
|
||||||
if (senderAddress != null && !Crypto.isValidAddress(senderAddress))
|
if ((txGroupId == null && involvingAddresses.size() != 2)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
|| (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);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
if (before != null && before < 1500000000000L)
|
if (before != null && before < 1500000000000L)
|
||||||
@ -87,12 +89,11 @@ public class ChatResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
return repository.getChatRepository().getTransactionsMatchingCriteria(
|
return repository.getChatRepository().getMessagesMatchingCriteria(
|
||||||
before,
|
before,
|
||||||
after,
|
after,
|
||||||
txGroupId,
|
txGroupId,
|
||||||
senderAddress,
|
involvingAddresses,
|
||||||
recipientAddress,
|
|
||||||
limit, offset, reverse);
|
limit, offset, reverse);
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, 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 java.util.List;
|
||||||
|
|
||||||
import org.qortal.data.transaction.ChatTransactionData;
|
import org.qortal.data.chat.ChatMessage;
|
||||||
|
|
||||||
public interface ChatRepository {
|
public interface ChatRepository {
|
||||||
|
|
||||||
public List<ChatTransactionData> getTransactionsMatchingCriteria(
|
/**
|
||||||
Long before,
|
* Returns CHAT messages matching criteria.
|
||||||
Long after,
|
* <p>
|
||||||
Integer txGroupId,
|
* Expects EITHER non-null txGroupID OR non-null sender and recipient addresses.
|
||||||
String senderAddress,
|
*/
|
||||||
String recipientAddress,
|
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after,
|
||||||
|
Integer txGroupId, List<String> involving,
|
||||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,9 @@ import java.sql.SQLException;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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.ChatRepository;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.transaction.Transaction.TransactionType;
|
|
||||||
|
|
||||||
public class HSQLDBChatRepository implements ChatRepository {
|
public class HSQLDBChatRepository implements ChatRepository {
|
||||||
|
|
||||||
@ -19,58 +18,48 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ChatTransactionData> getTransactionsMatchingCriteria(Long before, Long after, Integer txGroupId,
|
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId,
|
||||||
String senderAddress, String recipientAddress, Integer limit, Integer offset, Boolean reverse)
|
List<String> involving, Integer limit, Integer offset, Boolean reverse)
|
||||||
throws DataException {
|
throws DataException {
|
||||||
boolean hasSenderAddress = senderAddress != null && !senderAddress.isEmpty();
|
// Check args meet expectations
|
||||||
boolean hasRecipientAddress = recipientAddress != null && !recipientAddress.isEmpty();
|
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<String> whereClauses = new ArrayList<>();
|
||||||
List<Object> bindParams = 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
|
// Timestamp range
|
||||||
if (before != null) {
|
if (before != null) {
|
||||||
whereClauses.add("Transactions.created_when < ?");
|
whereClauses.add("created_when < ?");
|
||||||
bindParams.add(before);
|
bindParams.add(before);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (after != null) {
|
if (after != null) {
|
||||||
whereClauses.add("Transactions.created_when > ?");
|
whereClauses.add("created_when > ?");
|
||||||
bindParams.add(after);
|
bindParams.add(after);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (txGroupId != null)
|
if (txGroupId != null) {
|
||||||
whereClauses.add("Transactions.tx_group_id = " + txGroupId);
|
whereClauses.add("tx_group_id = " + txGroupId); // int safe to use literally
|
||||||
|
whereClauses.add("recipient IS NULL");
|
||||||
if (hasSenderAddress) {
|
} else {
|
||||||
whereClauses.add("ChatTransactions.sender = ?");
|
whereClauses.add("((sender = ? AND recipient = ?) OR (recipient = ? AND sender = ?))");
|
||||||
bindParams.add(senderAddress);
|
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()) {
|
if (!whereClauses.isEmpty()) {
|
||||||
sql.append(" WHERE ");
|
sql.append(" WHERE ");
|
||||||
|
|
||||||
@ -88,19 +77,31 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
|
|
||||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
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())) {
|
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||||
if (resultSet == null)
|
if (resultSet == null)
|
||||||
return chatTransactionsData;
|
return chatMessages;
|
||||||
|
|
||||||
do {
|
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());
|
} while (resultSet.next());
|
||||||
|
|
||||||
return chatTransactionsData;
|
return chatMessages;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to fetch matching chat transactions from repository", e);
|
throw new DataException("Unable to fetch matching chat transactions from repository", e);
|
||||||
}
|
}
|
||||||
|
@ -598,6 +598,10 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
// Chat transactions
|
// Chat transactions
|
||||||
stmt.execute("CREATE TABLE ChatTransactions (signature Signature, sender QortalAddress NOT NULL, nonce INT NOT NULL, recipient QortalAddress, "
|
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 + ")");
|
+ "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;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user