More work on CHAT API / support.

Renamed GET /chats/search to /chats/messages.

Added GET /chats/active/{address} to return lists of group chats
and direct chats involving {address}, where a chat message exists.
This commit is contained in:
catbref 2020-05-12 14:28:41 +01:00
parent 32470fa641
commit a9852e5305
5 changed files with 226 additions and 31 deletions

View File

@ -15,6 +15,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
@ -24,6 +25,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.ActiveChats;
import org.qortal.data.chat.ChatMessage; 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;
@ -49,7 +51,7 @@ public class ChatResource {
HttpServletRequest request; HttpServletRequest request;
@GET @GET
@Path("/search") @Path("/messages")
@Operation( @Operation(
summary = "Find chat messages", summary = "Find chat messages",
description = "Returns CHAT messages that match criteria. Must provide EITHER 'txGroupId' OR two 'involving' addresses.", description = "Returns CHAT messages that match criteria. Must provide EITHER 'txGroupId' OR two 'involving' addresses.",
@ -100,6 +102,34 @@ public class ChatResource {
} }
} }
@GET
@Path("/active/{address}")
@Operation(
summary = "Find active chats (group/direct) involving address",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ActiveChats.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public ActiveChats getActiveChats(@PathParam("address") String address) {
if (address == null || !Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getChatRepository().getActiveChats(address);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST @POST
@Operation( @Operation(
summary = "Build raw, unsigned, CHAT transaction", summary = "Build raw, unsigned, CHAT transaction",

View File

@ -0,0 +1,95 @@
package org.qortal.data.chat;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class ActiveChats {
@XmlAccessorType(XmlAccessType.FIELD)
public static class GroupChat {
private int groupId;
private String groupName;
private long timestamp;
protected GroupChat() {
/* JAXB */
}
public GroupChat(int groupId, String groupName, long timestamp) {
this.groupId = groupId;
this.groupName = groupName;
this.timestamp = timestamp;
}
public int getGroupId() {
return this.groupId;
}
public String getGroupName() {
return this.groupName;
}
public long getTimestamp() {
return this.timestamp;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class DirectChat {
private String address;
private String name;
private long timestamp;
protected DirectChat() {
/* JAXB */
}
public DirectChat(String address, String name, long timestamp) {
this.address = address;
this.name = name;
this.timestamp = timestamp;
}
public String getAddress() {
return this.address;
}
public String getName() {
return this.name;
}
public long getTimestamp() {
return this.timestamp;
}
}
// Properties
private List<GroupChat> groups;
private List<DirectChat> direct;
// Constructors
protected ActiveChats() {
/* For JAXB */
}
// For repository use
public ActiveChats(List<GroupChat> groups, List<DirectChat> direct) {
this.groups = groups;
this.direct = direct;
}
public List<GroupChat> getGroups() {
return this.groups;
}
public List<DirectChat> getDirect() {
return this.direct;
}
}

View File

@ -17,15 +17,9 @@ public class ChatMessage {
/* Address of sender */ /* Address of sender */
private String sender; private String sender;
/* Registered name of sender (if any) */
private String senderName; // can be null
/* Address of recipient (if any) */ /* Address of recipient (if any) */
private String recipient; // can be null private String recipient; // can be null
/* Registered name of recipient (if any) */
private String recipientName; // can be null
private byte[] data; private byte[] data;
private boolean isText; private boolean isText;
@ -38,15 +32,13 @@ public class ChatMessage {
} }
// For repository use // For repository use
public ChatMessage(long timestamp, int txGroupId, byte[] senderPublicKey, String sender, String senderName, public ChatMessage(long timestamp, int txGroupId, byte[] senderPublicKey, String sender,
String recipient, String recipientName, byte[] data, boolean isText, boolean isEncrypted) { String recipient, byte[] data, boolean isText, boolean isEncrypted) {
this.timestamp = timestamp; this.timestamp = timestamp;
this.txGroupId = txGroupId; this.txGroupId = txGroupId;
this.senderPublicKey = senderPublicKey; this.senderPublicKey = senderPublicKey;
this.sender = sender; this.sender = sender;
this.senderName = senderName;
this.recipient = recipient; this.recipient = recipient;
this.recipientName = recipientName;
this.data = data; this.data = data;
this.isText = isText; this.isText = isText;
this.isEncrypted = isEncrypted; this.isEncrypted = isEncrypted;
@ -68,18 +60,10 @@ public class ChatMessage {
return this.sender; return this.sender;
} }
public String getSenderName() {
return this.senderName;
}
public String getRecipient() { public String getRecipient() {
return this.recipient; return this.recipient;
} }
public String getRecipientName() {
return this.recipientName;
}
public byte[] getData() { public byte[] getData() {
return this.data; return this.data;
} }

View File

@ -2,6 +2,7 @@ package org.qortal.repository;
import java.util.List; import java.util.List;
import org.qortal.data.chat.ActiveChats;
import org.qortal.data.chat.ChatMessage; import org.qortal.data.chat.ChatMessage;
public interface ChatRepository { public interface ChatRepository {
@ -15,4 +16,6 @@ public interface ChatRepository {
Integer txGroupId, List<String> involving, Integer txGroupId, List<String> involving,
Integer limit, Integer offset, Boolean reverse) throws DataException; Integer limit, Integer offset, Boolean reverse) throws DataException;
public ActiveChats getActiveChats(String address) throws DataException;
} }

View File

@ -5,9 +5,13 @@ import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.qortal.data.chat.ActiveChats;
import org.qortal.data.chat.ActiveChats.DirectChat;
import org.qortal.data.chat.ActiveChats.GroupChat;
import org.qortal.data.chat.ChatMessage; 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 {
@ -28,12 +32,9 @@ public class HSQLDBChatRepository implements ChatRepository {
StringBuilder sql = new StringBuilder(1024); StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT created_when, tx_group_id, creator, sender, SenderNames.name, " sql.append("SELECT created_when, tx_group_id, creator, sender, recipient, data, is_text, is_encrypted "
+ "recipient, RecipientNames.name, data, is_text, is_encrypted "
+ "FROM ChatTransactions " + "FROM ChatTransactions "
+ "JOIN Transactions USING (signature) " + "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 // WHERE clauses
@ -88,15 +89,13 @@ public class HSQLDBChatRepository implements ChatRepository {
int groupId = resultSet.getInt(2); int groupId = resultSet.getInt(2);
byte[] senderPublicKey = resultSet.getBytes(3); byte[] senderPublicKey = resultSet.getBytes(3);
String sender = resultSet.getString(4); String sender = resultSet.getString(4);
String senderName = resultSet.getString(5); String recipient = resultSet.getString(5);
String recipient = resultSet.getString(6); byte[] data = resultSet.getBytes(6);
String recipientName = resultSet.getString(7); boolean isText = resultSet.getBoolean(7);
byte[] data = resultSet.getBytes(8); boolean isEncrypted = resultSet.getBoolean(8);
boolean isText = resultSet.getBoolean(9);
boolean isEncrypted = resultSet.getBoolean(10);
ChatMessage chatMessage = new ChatMessage(timestamp, groupId, senderPublicKey, sender, ChatMessage chatMessage = new ChatMessage(timestamp, groupId, senderPublicKey, sender,
senderName, recipient, recipientName, data, isText, isEncrypted); recipient, data, isText, isEncrypted);
chatMessages.add(chatMessage); chatMessages.add(chatMessage);
} while (resultSet.next()); } while (resultSet.next());
@ -107,4 +106,88 @@ public class HSQLDBChatRepository implements ChatRepository {
} }
} }
@Override
public ActiveChats getActiveChats(String address) throws DataException {
List<GroupChat> groupChats = getActiveGroupChats(address);
List<DirectChat> directChats = getActiveDirectChats(address);
return new ActiveChats(groupChats, directChats);
}
private List<GroupChat> getActiveGroupChats(String address) throws DataException {
// Find groups where address is a member and there is a chat
String groupsSql = "SELECT group_id, group_name, latest_timestamp "
+ "FROM GroupMembers "
+ "JOIN Groups USING (group_id) "
+ "CROSS JOIN LATERAL("
+ "SELECT created_when "
+ "FROM Transactions "
+ "WHERE tx_group_id = Groups.group_id AND type = " + TransactionType.CHAT.value + " "
+ "ORDER BY created_when DESC "
+ "LIMIT 1"
+ ") AS LatestMessages (latest_timestamp) "
+ "WHERE address = ?";
List<GroupChat> groupChats = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(groupsSql, address)) {
if (resultSet == null)
return groupChats;
do {
int groupId = resultSet.getInt(1);
String groupName = resultSet.getString(2);
long timestamp = resultSet.getLong(3);
GroupChat groupChat = new GroupChat(groupId, groupName, timestamp);
groupChats.add(groupChat);
} while (resultSet.next());
} catch (SQLException e) {
throw new DataException("Unable to fetch active group chats from repository", e);
}
return groupChats;
}
private List<DirectChat> getActiveDirectChats(String address) throws DataException {
// Find chat messages involving address
String directSql = "SELECT other_address, name, latest_timestamp "
+ "FROM ("
+ "SELECT recipient FROM ChatTransactions "
+ "WHERE sender = ? AND recipient IS NOT NULL "
+ "UNION "
+ "SELECT sender FROM ChatTransactions "
+ "WHERE recipient = ?"
+ ") AS OtherParties (other_address) "
+ "CROSS JOIN LATERAL("
+ "SELECT created_when FROM ChatTransactions "
+ "NATURAL JOIN Transactions "
+ "WHERE (sender = other_address AND recipient = ?) "
+ "OR (sender = ? AND recipient = other_address) "
+ "ORDER BY created_when DESC "
+ "LIMIT 1"
+ ") AS LatestMessages (latest_timestamp) "
+ "LEFT OUTER JOIN Names ON owner = other_address";
Object[] bindParams = new Object[] { address, address, address, address };
List<DirectChat> directChats = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(directSql, bindParams)) {
if (resultSet == null)
return directChats;
do {
String otherAddress = resultSet.getString(1);
String name = resultSet.getString(2);
long timestamp = resultSet.getLong(3);
DirectChat directChat = new DirectChat(otherAddress, name, timestamp);
directChats.add(directChat);
} while (resultSet.next());
} catch (SQLException e) {
throw new DataException("Unable to fetch active direct chats from repository", e);
}
return directChats;
}
} }