diff --git a/src/main/java/org/qortal/api/resource/ChatResource.java b/src/main/java/org/qortal/api/resource/ChatResource.java index 82dc2bec..4e60eb20 100644 --- a/src/main/java/org/qortal/api/resource/ChatResource.java +++ b/src/main/java/org/qortal/api/resource/ChatResource.java @@ -15,6 +15,7 @@ import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; @@ -24,6 +25,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.ActiveChats; import org.qortal.data.chat.ChatMessage; import org.qortal.data.transaction.ChatTransactionData; import org.qortal.data.transaction.TransactionData; @@ -49,7 +51,7 @@ public class ChatResource { HttpServletRequest request; @GET - @Path("/search") + @Path("/messages") @Operation( summary = "Find chat messages", 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 @Operation( summary = "Build raw, unsigned, CHAT transaction", diff --git a/src/main/java/org/qortal/data/chat/ActiveChats.java b/src/main/java/org/qortal/data/chat/ActiveChats.java new file mode 100644 index 00000000..59a13ca7 --- /dev/null +++ b/src/main/java/org/qortal/data/chat/ActiveChats.java @@ -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 groups; + + private List direct; + + // Constructors + + protected ActiveChats() { + /* For JAXB */ + } + + // For repository use + public ActiveChats(List groups, List direct) { + this.groups = groups; + this.direct = direct; + } + + public List getGroups() { + return this.groups; + } + + public List getDirect() { + return this.direct; + } + +} diff --git a/src/main/java/org/qortal/data/chat/ChatMessage.java b/src/main/java/org/qortal/data/chat/ChatMessage.java index 42ad57e9..73016c14 100644 --- a/src/main/java/org/qortal/data/chat/ChatMessage.java +++ b/src/main/java/org/qortal/data/chat/ChatMessage.java @@ -17,15 +17,9 @@ public class ChatMessage { /* 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; @@ -38,15 +32,13 @@ public class ChatMessage { } // 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) { + public ChatMessage(long timestamp, int txGroupId, byte[] senderPublicKey, String sender, + String recipient, 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; @@ -68,18 +60,10 @@ public class ChatMessage { 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; } diff --git a/src/main/java/org/qortal/repository/ChatRepository.java b/src/main/java/org/qortal/repository/ChatRepository.java index 504de8cb..8f3a6d51 100644 --- a/src/main/java/org/qortal/repository/ChatRepository.java +++ b/src/main/java/org/qortal/repository/ChatRepository.java @@ -2,6 +2,7 @@ package org.qortal.repository; import java.util.List; +import org.qortal.data.chat.ActiveChats; import org.qortal.data.chat.ChatMessage; public interface ChatRepository { @@ -15,4 +16,6 @@ public interface ChatRepository { Integer txGroupId, List involving, Integer limit, Integer offset, Boolean reverse) throws DataException; + public ActiveChats getActiveChats(String address) 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 08d141b0..528f71c2 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java @@ -5,9 +5,13 @@ import java.sql.SQLException; import java.util.ArrayList; 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.repository.ChatRepository; import org.qortal.repository.DataException; +import org.qortal.transaction.Transaction.TransactionType; public class HSQLDBChatRepository implements ChatRepository { @@ -28,12 +32,9 @@ public class HSQLDBChatRepository implements ChatRepository { 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 " + sql.append("SELECT created_when, tx_group_id, creator, sender, recipient, 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 "); + + "JOIN Transactions USING (signature) "); // WHERE clauses @@ -88,15 +89,13 @@ public class HSQLDBChatRepository implements ChatRepository { 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); + String recipient = resultSet.getString(5); + byte[] data = resultSet.getBytes(6); + boolean isText = resultSet.getBoolean(7); + boolean isEncrypted = resultSet.getBoolean(8); ChatMessage chatMessage = new ChatMessage(timestamp, groupId, senderPublicKey, sender, - senderName, recipient, recipientName, data, isText, isEncrypted); + recipient, data, isText, isEncrypted); chatMessages.add(chatMessage); } while (resultSet.next()); @@ -107,4 +106,88 @@ public class HSQLDBChatRepository implements ChatRepository { } } + @Override + public ActiveChats getActiveChats(String address) throws DataException { + List groupChats = getActiveGroupChats(address); + List directChats = getActiveDirectChats(address); + + return new ActiveChats(groupChats, directChats); + } + + private List 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 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 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 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; + } + }