From 90f1676c7cbe64dea0983f725e4b66d11b755ad5 Mon Sep 17 00:00:00 2001 From: catbref Date: Mon, 14 Jan 2019 14:36:43 +0000 Subject: [PATCH] Account group invite/cancel-invite and kick Account group join for a group that is closed/invite-only and has no corresponding invite is now turned into a "join request". This can be accepted by an admin sending a corresponding invite tx. --- .../org/qora/api/resource/GroupsResource.java | 187 +++++++++++++++++- .../org/qora/data/group/GroupInviteData.java | 60 ++++++ .../qora/data/group/GroupJoinRequestData.java | 35 ++++ .../CancelGroupInviteTransactionData.java | 84 ++++++++ .../GroupInviteTransactionData.java | 91 +++++++++ .../transaction/GroupKickTransactionData.java | 103 ++++++++++ .../data/transaction/TransactionData.java | 1 + src/main/java/org/qora/group/Group.java | 178 +++++++++++++++-- .../org/qora/repository/GroupRepository.java | 32 ++- .../repository/TransactionRepository.java | 5 + .../hsqldb/HSQLDBDatabaseUpdates.java | 54 ++--- .../hsqldb/HSQLDBGroupRepository.java | 180 ++++++++++++++++- ...ancelGroupInviteTransactionRepository.java | 50 +++++ ...SQLDBGroupInviteTransactionRepository.java | 76 +++++++ .../HSQLDBGroupKickTransactionRepository.java | 56 ++++++ .../HSQLDBTransactionRepository.java | 34 ++++ .../transaction/AddGroupAdminTransaction.java | 11 +- .../CancelGroupInviteTransaction.java | 157 +++++++++++++++ .../transaction/GroupInviteTransaction.java | 165 ++++++++++++++++ .../transaction/GroupKickTransaction.java | 165 ++++++++++++++++ .../transaction/JoinGroupTransaction.java | 2 + .../RemoveGroupAdminTransaction.java | 5 + .../org/qora/transaction/Transaction.java | 11 ++ ...ncelGroupInviteTransactionTransformer.java | 104 ++++++++++ .../GroupInviteTransactionTransformer.java | 109 ++++++++++ .../GroupKickTransactionTransformer.java | 110 +++++++++++ .../transaction/TransactionTransformer.java | 45 +++++ 27 files changed, 2066 insertions(+), 44 deletions(-) create mode 100644 src/main/java/org/qora/data/group/GroupInviteData.java create mode 100644 src/main/java/org/qora/data/group/GroupJoinRequestData.java create mode 100644 src/main/java/org/qora/data/transaction/CancelGroupInviteTransactionData.java create mode 100644 src/main/java/org/qora/data/transaction/GroupInviteTransactionData.java create mode 100644 src/main/java/org/qora/data/transaction/GroupKickTransactionData.java create mode 100644 src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCancelGroupInviteTransactionRepository.java create mode 100644 src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBGroupInviteTransactionRepository.java create mode 100644 src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBGroupKickTransactionRepository.java create mode 100644 src/main/java/org/qora/transaction/CancelGroupInviteTransaction.java create mode 100644 src/main/java/org/qora/transaction/GroupInviteTransaction.java create mode 100644 src/main/java/org/qora/transaction/GroupKickTransaction.java create mode 100644 src/main/java/org/qora/transform/transaction/CancelGroupInviteTransactionTransformer.java create mode 100644 src/main/java/org/qora/transform/transaction/GroupInviteTransactionTransformer.java create mode 100644 src/main/java/org/qora/transform/transaction/GroupKickTransactionTransformer.java diff --git a/src/main/java/org/qora/api/resource/GroupsResource.java b/src/main/java/org/qora/api/resource/GroupsResource.java index 1146fb3f..e7be882e 100644 --- a/src/main/java/org/qora/api/resource/GroupsResource.java +++ b/src/main/java/org/qora/api/resource/GroupsResource.java @@ -29,9 +29,14 @@ import org.qora.api.model.GroupWithMemberInfo; import org.qora.crypto.Crypto; import org.qora.data.group.GroupAdminData; import org.qora.data.group.GroupData; +import org.qora.data.group.GroupInviteData; +import org.qora.data.group.GroupJoinRequestData; import org.qora.data.group.GroupMemberData; import org.qora.data.transaction.AddGroupAdminTransactionData; +import org.qora.data.transaction.CancelGroupInviteTransactionData; import org.qora.data.transaction.CreateGroupTransactionData; +import org.qora.data.transaction.GroupInviteTransactionData; +import org.qora.data.transaction.GroupKickTransactionData; import org.qora.data.transaction.JoinGroupTransactionData; import org.qora.data.transaction.LeaveGroupTransactionData; import org.qora.data.transaction.RemoveGroupAdminTransactionData; @@ -43,7 +48,10 @@ import org.qora.transaction.Transaction; import org.qora.transaction.Transaction.ValidationResult; import org.qora.transform.TransformationException; import org.qora.transform.transaction.AddGroupAdminTransactionTransformer; +import org.qora.transform.transaction.CancelGroupInviteTransactionTransformer; import org.qora.transform.transaction.CreateGroupTransactionTransformer; +import org.qora.transform.transaction.GroupInviteTransactionTransformer; +import org.qora.transform.transaction.GroupKickTransactionTransformer; import org.qora.transform.transaction.JoinGroupTransactionTransformer; import org.qora.transform.transaction.LeaveGroupTransactionTransformer; import org.qora.transform.transaction.RemoveGroupAdminTransactionTransformer; @@ -140,7 +148,7 @@ public class GroupsResource { Integer memberCount = null; if (includeMembers) { - groupMembers = repository.getGroupRepository().getAllGroupMembers(groupData.getGroupName()); + groupMembers = repository.getGroupRepository().getGroupMembers(groupData.getGroupName()); // Strip groupName from member info groupMembers = groupMembers.stream().map(groupMemberData -> new GroupMemberData(null, groupMemberData.getMember(), groupMemberData.getJoined(), null)).collect(Collectors.toList()); @@ -152,7 +160,7 @@ public class GroupsResource { } // Always include admins - List groupAdmins = repository.getGroupRepository().getAllGroupAdmins(groupData.getGroupName()); + List groupAdmins = repository.getGroupRepository().getGroupAdmins(groupData.getGroupName()); // We only need admin addresses List groupAdminAddresses = groupAdmins.stream().map(groupAdminData -> groupAdminData.getAdmin()).collect(Collectors.toList()); @@ -336,6 +344,135 @@ public class GroupsResource { } } + @POST + @Path("/kick") + @Operation( + summary = "Build raw, unsigned, GROUP_KICK transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = GroupKickTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, GROUP_KICK transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + public String groupKick(GroupKickTransactionData transactionData) { + try (final Repository repository = RepositoryManager.getRepository()) { + Transaction transaction = Transaction.fromData(repository, transactionData); + + ValidationResult result = transaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = GroupKickTransactionTransformer.toBytes(transactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/invite") + @Operation( + summary = "Build raw, unsigned, GROUP_INVITE transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = GroupInviteTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, GROUP_INVITE transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + public String groupInvite(GroupInviteTransactionData transactionData) { + try (final Repository repository = RepositoryManager.getRepository()) { + Transaction transaction = Transaction.fromData(repository, transactionData); + + ValidationResult result = transaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = GroupInviteTransactionTransformer.toBytes(transactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/invite/cancel") + @Operation( + summary = "Build raw, unsigned, CANCEL_GROUP_INVITE transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CancelGroupInviteTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, CANCEL_GROUP_INVITE transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + public String cancelGroupInvite(CancelGroupInviteTransactionData transactionData) { + try (final Repository repository = RepositoryManager.getRepository()) { + Transaction transaction = Transaction.fromData(repository, transactionData); + + ValidationResult result = transaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = CancelGroupInviteTransactionTransformer.toBytes(transactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + @POST @Path("/join") @Operation( @@ -422,4 +559,50 @@ public class GroupsResource { } } + @GET + @Path("/invites/{groupname}") + @Operation( + summary = "Pending group invites", + responses = { + @ApiResponse( + description = "group invite", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = GroupInviteData.class) + ) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public List getInvites(@PathParam("groupname") String groupName) { + try (final Repository repository = RepositoryManager.getRepository()) { + return repository.getGroupRepository().getGroupInvites(groupName); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/joinrequests/{groupname}") + @Operation( + summary = "Pending group join requests", + responses = { + @ApiResponse( + description = "group jon requests", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = GroupJoinRequestData.class) + ) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public List getJoinRequests(@PathParam("groupname") String groupName) { + try (final Repository repository = RepositoryManager.getRepository()) { + return repository.getGroupRepository().getGroupJoinRequests(groupName); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + } \ No newline at end of file diff --git a/src/main/java/org/qora/data/group/GroupInviteData.java b/src/main/java/org/qora/data/group/GroupInviteData.java new file mode 100644 index 00000000..9e4acccb --- /dev/null +++ b/src/main/java/org/qora/data/group/GroupInviteData.java @@ -0,0 +1,60 @@ +package org.qora.data.group; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; + +// All properties to be converted to JSON via JAX-RS +@XmlAccessorType(XmlAccessType.FIELD) +public class GroupInviteData { + + // Properties + private String groupName; + private String inviter; + private String invitee; + private Long expiry; + // No need to ever expose this via API + @XmlTransient + private byte[] reference; + + // Constructors + + // necessary for JAX-RS serialization + protected GroupInviteData() { + } + + public GroupInviteData(String groupName, String inviter, String invitee, Long expiry, byte[] reference) { + this.groupName = groupName; + this.inviter = inviter; + this.invitee = invitee; + this.expiry = expiry; + this.reference = reference; + } + + // Getters / setters + + public String getGroupName() { + return this.groupName; + } + + public String getInviter() { + return this.inviter; + } + + public String getInvitee() { + return this.invitee; + } + + public Long getExpiry() { + return this.expiry; + } + + public byte[] getReference() { + return this.reference; + } + + public void setReference(byte[] reference) { + this.reference = reference; + } + +} diff --git a/src/main/java/org/qora/data/group/GroupJoinRequestData.java b/src/main/java/org/qora/data/group/GroupJoinRequestData.java new file mode 100644 index 00000000..2c275c74 --- /dev/null +++ b/src/main/java/org/qora/data/group/GroupJoinRequestData.java @@ -0,0 +1,35 @@ +package org.qora.data.group; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +// All properties to be converted to JSON via JAX-RS +@XmlAccessorType(XmlAccessType.FIELD) +public class GroupJoinRequestData { + + // Properties + private String groupName; + private String joiner; + + // Constructors + + // necessary for JAX-RS serialization + protected GroupJoinRequestData() { + } + + public GroupJoinRequestData(String groupName, String joiner) { + this.groupName = groupName; + this.joiner = joiner; + } + + // Getters / setters + + public String getGroupName() { + return this.groupName; + } + + public String getJoiner() { + return this.joiner; + } + +} diff --git a/src/main/java/org/qora/data/transaction/CancelGroupInviteTransactionData.java b/src/main/java/org/qora/data/transaction/CancelGroupInviteTransactionData.java new file mode 100644 index 00000000..cfd5b73f --- /dev/null +++ b/src/main/java/org/qora/data/transaction/CancelGroupInviteTransactionData.java @@ -0,0 +1,84 @@ +package org.qora.data.transaction; + +import java.math.BigDecimal; + +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; + +import org.qora.transaction.Transaction.TransactionType; + +import io.swagger.v3.oas.annotations.media.Schema; + +// All properties to be converted to JSON via JAX-RS +@XmlAccessorType(XmlAccessType.FIELD) +@Schema(allOf = { TransactionData.class }) +public class CancelGroupInviteTransactionData extends TransactionData { + + // Properties + @Schema(description = "admin's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + private byte[] adminPublicKey; + @Schema(description = "group name", example = "my-group") + private String groupName; + @Schema(description = "invitee's address", example = "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK") + private String invitee; + // No need to ever expose this via API + @XmlTransient + private byte[] groupReference; + + // Constructors + + // For JAX-RS + protected CancelGroupInviteTransactionData() { + super(TransactionType.CANCEL_GROUP_INVITE); + } + + public void afterUnmarshal(Unmarshaller u, Object parent) { + this.creatorPublicKey = this.adminPublicKey; + } + + public CancelGroupInviteTransactionData(byte[] adminPublicKey, String groupName, String invitee, byte[] groupReference, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(TransactionType.CANCEL_GROUP_INVITE, fee, adminPublicKey, timestamp, reference, signature); + + this.adminPublicKey = adminPublicKey; + this.groupName = groupName; + this.invitee = invitee; + this.groupReference = groupReference; + } + + public CancelGroupInviteTransactionData(byte[] adminPublicKey, String groupName, String invitee, byte[] groupReference, BigDecimal fee, long timestamp, byte[] reference) { + this(adminPublicKey, groupName, invitee, groupReference, fee, timestamp, reference, null); + } + + public CancelGroupInviteTransactionData(byte[] adminPublicKey, String groupName, String invitee, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + this(adminPublicKey, groupName, invitee, null, fee, timestamp, reference, signature); + } + + public CancelGroupInviteTransactionData(byte[] adminPublicKey, String groupName, String invitee, BigDecimal fee, long timestamp, byte[] reference) { + this(adminPublicKey, groupName, invitee, null, fee, timestamp, reference, null); + } + + // Getters / setters + + public byte[] getAdminPublicKey() { + return this.adminPublicKey; + } + + public String getGroupName() { + return this.groupName; + } + + public String getInvitee() { + return this.invitee; + } + + public byte[] getGroupReference() { + return this.groupReference; + } + + public void setGroupReference(byte[] groupReference) { + this.groupReference = groupReference; + } + +} \ No newline at end of file diff --git a/src/main/java/org/qora/data/transaction/GroupInviteTransactionData.java b/src/main/java/org/qora/data/transaction/GroupInviteTransactionData.java new file mode 100644 index 00000000..a15e79d7 --- /dev/null +++ b/src/main/java/org/qora/data/transaction/GroupInviteTransactionData.java @@ -0,0 +1,91 @@ +package org.qora.data.transaction; + +import java.math.BigDecimal; + +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; + +import org.qora.transaction.Transaction.TransactionType; + +import io.swagger.v3.oas.annotations.media.Schema; + +// All properties to be converted to JSON via JAX-RS +@XmlAccessorType(XmlAccessType.FIELD) +@Schema(allOf = { TransactionData.class }) +public class GroupInviteTransactionData extends TransactionData { + + // Properties + @Schema(description = "admin's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + private byte[] adminPublicKey; + @Schema(description = "group name", example = "my-group") + private String groupName; + @Schema(description = "invitee's address", example = "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK") + private String invitee; + @Schema(description = "invitation lifetime in seconds") + private int timeToLive; + // No need to ever expose this via API + @XmlTransient + private byte[] groupReference; + + // Constructors + + // For JAX-RS + protected GroupInviteTransactionData() { + super(TransactionType.GROUP_INVITE); + } + + public void afterUnmarshal(Unmarshaller u, Object parent) { + this.creatorPublicKey = this.adminPublicKey; + } + + public GroupInviteTransactionData(byte[] adminPublicKey, String groupName, String invitee, int timeToLive, byte[] groupReference, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(TransactionType.GROUP_INVITE, fee, adminPublicKey, timestamp, reference, signature); + + this.adminPublicKey = adminPublicKey; + this.groupName = groupName; + this.invitee = invitee; + this.timeToLive = timeToLive; + this.groupReference = groupReference; + } + + public GroupInviteTransactionData(byte[] adminPublicKey, String groupName, String invitee, int timeToLive, byte[] groupReference, BigDecimal fee, long timestamp, byte[] reference) { + this(adminPublicKey, groupName, invitee, timeToLive, groupReference, fee, timestamp, reference, null); + } + + public GroupInviteTransactionData(byte[] adminPublicKey, String groupName, String invitee, int timeToLive, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + this(adminPublicKey, groupName, invitee, timeToLive, null, fee, timestamp, reference, signature); + } + + public GroupInviteTransactionData(byte[] adminPublicKey, String groupName, String invitee, int timeToLive, BigDecimal fee, long timestamp, byte[] reference) { + this(adminPublicKey, groupName, invitee, timeToLive, null, fee, timestamp, reference, null); + } + + // Getters / setters + + public byte[] getAdminPublicKey() { + return this.adminPublicKey; + } + + public String getGroupName() { + return this.groupName; + } + + public String getInvitee() { + return this.invitee; + } + + public int getTimeToLive() { + return this.timeToLive; + } + + public byte[] getGroupReference() { + return this.groupReference; + } + + public void setGroupReference(byte[] groupReference) { + this.groupReference = groupReference; + } + +} \ No newline at end of file diff --git a/src/main/java/org/qora/data/transaction/GroupKickTransactionData.java b/src/main/java/org/qora/data/transaction/GroupKickTransactionData.java new file mode 100644 index 00000000..7d465f07 --- /dev/null +++ b/src/main/java/org/qora/data/transaction/GroupKickTransactionData.java @@ -0,0 +1,103 @@ +package org.qora.data.transaction; + +import java.math.BigDecimal; + +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; + +import org.qora.transaction.Transaction.TransactionType; + +import io.swagger.v3.oas.annotations.media.Schema; + +// All properties to be converted to JSON via JAX-RS +@XmlAccessorType(XmlAccessType.FIELD) +@Schema(allOf = { TransactionData.class }) +public class GroupKickTransactionData extends TransactionData { + + // Properties + @Schema(description = "admin's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + private byte[] adminPublicKey; + @Schema(description = "group name", example = "my-group") + private String groupName; + @Schema(description = "member to kick from group", example = "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK") + private String member; + @Schema(description = "reason for kick") + private String reason; + // No need to ever expose this via API + @XmlTransient + private byte[] memberReference; + // No need to ever expose this via API + @XmlTransient + private byte[] adminReference; + + // Constructors + + // For JAX-RS + protected GroupKickTransactionData() { + super(TransactionType.GROUP_KICK); + } + + public void afterUnmarshal(Unmarshaller u, Object parent) { + this.creatorPublicKey = this.adminPublicKey; + } + + public GroupKickTransactionData(byte[] adminPublicKey, String groupName, String member, String reason, byte[] memberReference, byte[] adminReference, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(TransactionType.GROUP_KICK, fee, adminPublicKey, timestamp, reference, signature); + + this.adminPublicKey = adminPublicKey; + this.groupName = groupName; + this.member = member; + this.reason = reason; + this.memberReference = memberReference; + this.adminReference = adminReference; + } + + public GroupKickTransactionData(byte[] adminPublicKey, String groupName, String member, String reason, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + this(adminPublicKey, groupName, member, reason, null, null, fee, timestamp, reference, signature); + } + + public GroupKickTransactionData(byte[] adminPublicKey, String groupName, String member, String reason, byte[] memberReference, byte[] adminReference, BigDecimal fee, long timestamp, byte[] reference) { + this(adminPublicKey, groupName, member, reason, memberReference, adminReference, fee, timestamp, reference, null); + } + + public GroupKickTransactionData(byte[] adminPublicKey, String groupName, String member, String reason, BigDecimal fee, long timestamp, byte[] reference) { + this(adminPublicKey, groupName, member, reason, null, null, fee, timestamp, reference, null); + } + + // Getters / setters + + public byte[] getAdminPublicKey() { + return this.adminPublicKey; + } + + public String getGroupName() { + return this.groupName; + } + + public String getMember() { + return this.member; + } + + public String getReason() { + return this.reason; + } + + public byte[] getMemberReference() { + return this.memberReference; + } + + public void setMemberReference(byte[] memberReference) { + this.memberReference = memberReference; + } + + public byte[] getAdminReference() { + return this.adminReference; + } + + public void setAdminReference(byte[] adminReference) { + this.adminReference = adminReference; + } + +} diff --git a/src/main/java/org/qora/data/transaction/TransactionData.java b/src/main/java/org/qora/data/transaction/TransactionData.java index a860a7a8..32d07eb1 100644 --- a/src/main/java/org/qora/data/transaction/TransactionData.java +++ b/src/main/java/org/qora/data/transaction/TransactionData.java @@ -35,6 +35,7 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode; MultiPaymentTransactionData.class, DeployATTransactionData.class, MessageTransactionData.class, ATTransactionData.class, CreateGroupTransactionData.class, UpdateGroupTransactionData.class, AddGroupAdminTransactionData.class, RemoveGroupAdminTransactionData.class, + GroupKickTransactionData.class, GroupInviteTransactionData.class, JoinGroupTransactionData.class, LeaveGroupTransactionData.class }) //All properties to be converted to JSON via JAX-RS diff --git a/src/main/java/org/qora/group/Group.java b/src/main/java/org/qora/group/Group.java index f578fb05..6237a7e7 100644 --- a/src/main/java/org/qora/group/Group.java +++ b/src/main/java/org/qora/group/Group.java @@ -1,14 +1,20 @@ package org.qora.group; import java.util.Arrays; +import java.util.List; import org.qora.account.Account; import org.qora.account.PublicKeyAccount; import org.qora.data.group.GroupAdminData; import org.qora.data.group.GroupData; +import org.qora.data.group.GroupInviteData; +import org.qora.data.group.GroupJoinRequestData; import org.qora.data.group.GroupMemberData; import org.qora.data.transaction.AddGroupAdminTransactionData; +import org.qora.data.transaction.CancelGroupInviteTransactionData; import org.qora.data.transaction.CreateGroupTransactionData; +import org.qora.data.transaction.GroupInviteTransactionData; +import org.qora.data.transaction.GroupKickTransactionData; import org.qora.data.transaction.JoinGroupTransactionData; import org.qora.data.transaction.LeaveGroupTransactionData; import org.qora.data.transaction.RemoveGroupAdminTransactionData; @@ -27,6 +33,8 @@ public class Group { // Useful constants public static final int MAX_NAME_SIZE = 400; public static final int MAX_DESCRIPTION_SIZE = 4000; + /** Max size of kick/ban reason */ + public static final int MAX_REASON_SIZE = 400; // Constructors @@ -38,9 +46,9 @@ public class Group { */ public Group(Repository repository, CreateGroupTransactionData createGroupTransactionData) { this.repository = repository; - this.groupData = new GroupData(createGroupTransactionData.getOwner(), - createGroupTransactionData.getGroupName(), createGroupTransactionData.getDescription(), createGroupTransactionData.getTimestamp(), - createGroupTransactionData.getIsOpen(), createGroupTransactionData.getSignature()); + this.groupData = new GroupData(createGroupTransactionData.getOwner(), createGroupTransactionData.getGroupName(), + createGroupTransactionData.getDescription(), createGroupTransactionData.getTimestamp(), createGroupTransactionData.getIsOpen(), + createGroupTransactionData.getSignature()); } /** @@ -61,10 +69,12 @@ public class Group { this.repository.getGroupRepository().save(this.groupData); // Add owner as admin too - this.repository.getGroupRepository().save(new GroupAdminData(this.groupData.getGroupName(), this.groupData.getOwner(), createGroupTransactionData.getSignature())); + this.repository.getGroupRepository() + .save(new GroupAdminData(this.groupData.getGroupName(), this.groupData.getOwner(), createGroupTransactionData.getSignature())); // Add owner as member too - this.repository.getGroupRepository().save(new GroupMemberData(this.groupData.getGroupName(), this.groupData.getOwner(), this.groupData.getCreated(), createGroupTransactionData.getSignature())); + this.repository.getGroupRepository().save(new GroupMemberData(this.groupData.getGroupName(), this.groupData.getOwner(), this.groupData.getCreated(), + createGroupTransactionData.getSignature())); } public void uncreate() throws DataException { @@ -124,7 +134,8 @@ public class Group { // New owner should be a member if not already if (!groupRepository.memberExists(groupName, newOwner)) { - GroupMemberData groupMemberData = new GroupMemberData(groupName, newOwner, updateGroupTransactionData.getTimestamp(), updateGroupTransactionData.getSignature()); + GroupMemberData groupMemberData = new GroupMemberData(groupName, newOwner, updateGroupTransactionData.getTimestamp(), + updateGroupTransactionData.getSignature()); groupRepository.save(groupMemberData); } @@ -168,7 +179,8 @@ public class Group { } public void promoteToAdmin(AddGroupAdminTransactionData addGroupAdminTransactionData) throws DataException { - GroupAdminData groupAdminData = new GroupAdminData(addGroupAdminTransactionData.getGroupName(), addGroupAdminTransactionData.getMember(), addGroupAdminTransactionData.getSignature()); + GroupAdminData groupAdminData = new GroupAdminData(addGroupAdminTransactionData.getGroupName(), addGroupAdminTransactionData.getMember(), + addGroupAdminTransactionData.getSignature()); this.repository.getGroupRepository().save(groupAdminData); } @@ -199,17 +211,160 @@ public class Group { groupRepository.save(groupAdminData); } + public void kick(GroupKickTransactionData groupKickTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = groupKickTransactionData.getGroupName(); + String member = groupKickTransactionData.getMember(); + + // Store membership and (optionally) adminship transactions for orphaning purposes + GroupAdminData groupAdminData = groupRepository.getAdmin(groupName, member); + if (groupAdminData != null) { + groupKickTransactionData.setAdminReference(groupAdminData.getGroupReference()); + + groupRepository.deleteAdmin(groupName, member); + } + + GroupMemberData groupMemberData = groupRepository.getMember(groupName, member); + groupKickTransactionData.setMemberReference(groupMemberData.getGroupReference()); + + groupRepository.deleteMember(groupName, member); + } + + public void unkick(GroupKickTransactionData groupKickTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = groupKickTransactionData.getGroupName(); + String member = groupKickTransactionData.getMember(); + + // Rebuild member entry using stored transaction reference + TransactionData membershipTransactionData = this.repository.getTransactionRepository().fromSignature(groupKickTransactionData.getMemberReference()); + GroupMemberData groupMemberData = new GroupMemberData(groupName, member, membershipTransactionData.getTimestamp(), + membershipTransactionData.getSignature()); + groupRepository.save(groupMemberData); + + if (groupKickTransactionData.getAdminReference() != null) { + // Rebuild admin entry using stored transaction reference + GroupAdminData groupAdminData = new GroupAdminData(groupName, member, groupKickTransactionData.getAdminReference()); + groupRepository.save(groupAdminData); + } + } + + public void invite(GroupInviteTransactionData groupInviteTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = groupInviteTransactionData.getGroupName(); + Account inviter = new PublicKeyAccount(this.repository, groupInviteTransactionData.getAdminPublicKey()); + + // If there is a pending "join request" then add new group member + if (groupRepository.joinRequestExists(groupName, groupInviteTransactionData.getInvitee())) { + GroupMemberData groupMemberData = new GroupMemberData(groupName, groupInviteTransactionData.getInvitee(), groupInviteTransactionData.getTimestamp(), + groupInviteTransactionData.getSignature()); + groupRepository.save(groupMemberData); + + // Delete join request + groupRepository.deleteJoinRequest(groupName, groupInviteTransactionData.getInvitee()); + + return; + } + + Long expiry; + int timeToLive = groupInviteTransactionData.getTimeToLive(); + if (timeToLive == 0) + expiry = null; + else + expiry = groupInviteTransactionData.getTimestamp() + timeToLive; + + GroupInviteData groupInviteData = new GroupInviteData(groupName, inviter.getAddress(), groupInviteTransactionData.getInvitee(), expiry, + groupInviteTransactionData.getSignature()); + groupRepository.save(groupInviteData); + } + + public void uninvite(GroupInviteTransactionData groupInviteTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = groupInviteTransactionData.getGroupName(); + Account inviter = new PublicKeyAccount(this.repository, groupInviteTransactionData.getAdminPublicKey()); + String invitee = groupInviteTransactionData.getInvitee(); + + // Put back any "join request" + if (groupRepository.memberExists(groupName, invitee)) { + GroupJoinRequestData groupJoinRequestData = new GroupJoinRequestData(groupName, invitee); + groupRepository.save(groupJoinRequestData); + + // Delete member + groupRepository.deleteMember(groupName, invitee); + } + + // Delete invite + groupRepository.deleteInvite(groupName, inviter.getAddress(), invitee); + } + + public void cancelInvite(CancelGroupInviteTransactionData cancelGroupInviteTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = cancelGroupInviteTransactionData.getGroupName(); + Account inviter = new PublicKeyAccount(this.repository, cancelGroupInviteTransactionData.getAdminPublicKey()); + String invitee = cancelGroupInviteTransactionData.getInvitee(); + + // Save invite's transaction signature for orphaning purposes + GroupInviteData groupInviteData = groupRepository.getInvite(groupName, inviter.getAddress(), invitee); + cancelGroupInviteTransactionData.setGroupReference(groupInviteData.getReference()); + + // Delete invite + groupRepository.deleteInvite(groupName, inviter.getAddress(), invitee); + } + + public void uncancelInvite(CancelGroupInviteTransactionData cancelGroupInviteTransactionData) throws DataException { + // Reinstate invite + TransactionData transactionData = this.repository.getTransactionRepository().fromSignature(cancelGroupInviteTransactionData.getGroupReference()); + invite((GroupInviteTransactionData) transactionData); + + cancelGroupInviteTransactionData.setGroupReference(null); + } + public void join(JoinGroupTransactionData joinGroupTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = joinGroupTransactionData.getGroupName(); Account joiner = new PublicKeyAccount(this.repository, joinGroupTransactionData.getJoinerPublicKey()); - GroupMemberData groupMemberData = new GroupMemberData(joinGroupTransactionData.getGroupName(), joiner.getAddress(), joinGroupTransactionData.getTimestamp(), joinGroupTransactionData.getSignature()); - this.repository.getGroupRepository().save(groupMemberData); + // Set invite transactions' group-reference to this transaction's signature so the invites can be put back if we orphan this join + // Delete any pending invites + List invites = groupRepository.getInvitesByInvitee(groupName, joiner.getAddress()); + for (GroupInviteData invite : invites) { + TransactionData transactionData = this.repository.getTransactionRepository().fromSignature(invite.getReference()); + ((GroupInviteTransactionData) transactionData).setGroupReference(joinGroupTransactionData.getSignature()); + + groupRepository.deleteInvite(groupName, invite.getInviter(), joiner.getAddress()); + } + + // If there were no invites and this group is "closed" (i.e. invite-only) then + // this is now a pending "join request" + if (invites.isEmpty() && !groupData.getIsOpen()) { + GroupJoinRequestData groupJoinRequestData = new GroupJoinRequestData(groupName, joiner.getAddress()); + groupRepository.save(groupJoinRequestData); + return; + } + + // Actually add new member to group + GroupMemberData groupMemberData = new GroupMemberData(groupName, joiner.getAddress(), joinGroupTransactionData.getTimestamp(), + joinGroupTransactionData.getSignature()); + groupRepository.save(groupMemberData); + } public void unjoin(JoinGroupTransactionData joinGroupTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = joinGroupTransactionData.getGroupName(); Account joiner = new PublicKeyAccount(this.repository, joinGroupTransactionData.getJoinerPublicKey()); - this.repository.getGroupRepository().deleteMember(joinGroupTransactionData.getGroupName(), joiner.getAddress()); + groupRepository.deleteMember(groupName, joiner.getAddress()); + + // Put back any pending invites + List inviteTransactions = this.repository.getTransactionRepository() + .getInvitesWithGroupReference(joinGroupTransactionData.getSignature()); + for (GroupInviteTransactionData inviteTransaction : inviteTransactions) { + this.invite(inviteTransaction); + + // Remove group-reference + inviteTransaction.setGroupReference(null); + this.repository.getTransactionRepository().save(inviteTransaction); + } } public void leave(LeaveGroupTransactionData leaveGroupTransactionData) throws DataException { @@ -249,7 +404,8 @@ public class Group { // Rejoin as member TransactionData membershipTransactionData = this.repository.getTransactionRepository().fromSignature(leaveGroupTransactionData.getMemberReference()); - groupRepository.save(new GroupMemberData(groupName, leaver.getAddress(), membershipTransactionData.getTimestamp(), membershipTransactionData.getSignature())); + groupRepository + .save(new GroupMemberData(groupName, leaver.getAddress(), membershipTransactionData.getTimestamp(), membershipTransactionData.getSignature())); // Put back any admin state based on referenced group-related transaction byte[] adminTransactionSignature = leaveGroupTransactionData.getAdminReference(); diff --git a/src/main/java/org/qora/repository/GroupRepository.java b/src/main/java/org/qora/repository/GroupRepository.java index 15efe819..d352581a 100644 --- a/src/main/java/org/qora/repository/GroupRepository.java +++ b/src/main/java/org/qora/repository/GroupRepository.java @@ -4,6 +4,8 @@ import java.util.List; import org.qora.data.group.GroupAdminData; import org.qora.data.group.GroupData; +import org.qora.data.group.GroupInviteData; +import org.qora.data.group.GroupJoinRequestData; import org.qora.data.group.GroupMemberData; public interface GroupRepository { @@ -28,7 +30,7 @@ public interface GroupRepository { public boolean adminExists(String groupName, String address) throws DataException; - public List getAllGroupAdmins(String groupName) throws DataException; + public List getGroupAdmins(String groupName) throws DataException; public void save(GroupAdminData groupAdminData) throws DataException; @@ -40,7 +42,7 @@ public interface GroupRepository { public boolean memberExists(String groupName, String address) throws DataException; - public List getAllGroupMembers(String groupName) throws DataException; + public List getGroupMembers(String groupName) throws DataException; /** Returns number of group members, or null if group doesn't exist */ public Integer countGroupMembers(String groupName) throws DataException; @@ -49,4 +51,30 @@ public interface GroupRepository { public void deleteMember(String groupName, String address) throws DataException; + // Group Invites + + public GroupInviteData getInvite(String groupName, String inviter, String invitee) throws DataException; + + public boolean hasInvite(String groupName, String invitee) throws DataException; + + public boolean inviteExists(String groupName, String inviter, String invitee) throws DataException; + + public List getGroupInvites(String groupName) throws DataException; + + public List getInvitesByInvitee(String groupName, String invitee) throws DataException; + + public void save(GroupInviteData groupInviteData) throws DataException; + + public void deleteInvite(String groupName, String inviter, String invitee) throws DataException; + + // Group Join Requests + + public boolean joinRequestExists(String groupName, String joiner) throws DataException; + + public List getGroupJoinRequests(String groupName) throws DataException; + + public void save(GroupJoinRequestData groupJoinRequestData) throws DataException; + + public void deleteJoinRequest(String groupName, String joiner) throws DataException; + } \ No newline at end of file diff --git a/src/main/java/org/qora/repository/TransactionRepository.java b/src/main/java/org/qora/repository/TransactionRepository.java index afaffe69..2ae7f0f0 100644 --- a/src/main/java/org/qora/repository/TransactionRepository.java +++ b/src/main/java/org/qora/repository/TransactionRepository.java @@ -2,6 +2,7 @@ package org.qora.repository; import java.util.List; +import org.qora.data.transaction.GroupInviteTransactionData; import org.qora.data.transaction.TransactionData; import org.qora.transaction.Transaction.TransactionType; @@ -52,4 +53,8 @@ public interface TransactionRepository { public void delete(TransactionData transactionData) throws DataException; + /** Returns transaction data for group invite transactions that have groupReference that matches passed value. + * @throws DataException */ + public List getInvitesWithGroupReference(byte[] groupReference) throws DataException; + } diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java index 82970ea1..812acb0b 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -393,57 +393,48 @@ public class HSQLDBDatabaseUpdates { break; case 28: - // XXX TEMP fixes to registered names - remove before database rebuild! - // Allow name_reference to be NULL while transaction is unconfirmed - stmt.execute("ALTER TABLE UpdateNameTransactions ALTER COLUMN name_reference SET NULL"); - stmt.execute("ALTER TABLE BuyNameTransactions ALTER COLUMN name_reference SET NULL"); - // Names.registrant shouldn't be there - stmt.execute("ALTER TABLE Names DROP COLUMN registrant"); - break; - - case 29: - // XXX TEMP bridging statements for AccountGroups - remove before database rebuild! - stmt.execute("CREATE TYPE GenericDescription AS VARCHAR(4000)"); - stmt.execute("CREATE TYPE GroupName AS VARCHAR(400) COLLATE SQL_TEXT_UCC_NO_PAD"); - break; - - case 30: // Account groups stmt.execute("CREATE TABLE AccountGroups (group_name GroupName, owner QoraAddress NOT NULL, description GenericDescription NOT NULL, " + "created TIMESTAMP WITH TIME ZONE NOT NULL, updated TIMESTAMP WITH TIME ZONE, is_open BOOLEAN NOT NULL, " + "reference Signature, PRIMARY KEY (group_name))"); // For finding groups by owner - stmt.execute("CREATE INDEX AccountGroupOwnerIndex on AccountGroups (owner)"); + stmt.execute("CREATE INDEX AccountGroupOwnerIndex ON AccountGroups (owner)"); // Admins stmt.execute("CREATE TABLE AccountGroupAdmins (group_name GroupName, admin QoraAddress, group_reference Signature NOT NULL, PRIMARY KEY (group_name, admin))"); // For finding groups that address administrates - stmt.execute("CREATE INDEX AccountGroupAdminIndex on AccountGroupAdmins (admin)"); + stmt.execute("CREATE INDEX AccountGroupAdminIndex ON AccountGroupAdmins (admin)"); // Members stmt.execute("CREATE TABLE AccountGroupMembers (group_name GroupName, address QoraAddress, joined TIMESTAMP WITH TIME ZONE NOT NULL, group_reference Signature NOT NULL, " + "PRIMARY KEY (group_name, address))"); // For finding groups that address is member - stmt.execute("CREATE INDEX AccountGroupMemberIndex on AccountGroupMembers (address)"); + stmt.execute("CREATE INDEX AccountGroupMemberIndex ON AccountGroupMembers (address)"); // Invites // PRIMARY KEY (invitee + group + inviter) because most queries will be "what have I been invited to?" from UI - stmt.execute("CREATE TABLE AccountGroupInvites (group_name GroupName, invitee QoraAddress, inviter QoraAddress, " - + "expiry TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (invitee, group_name, inviter))"); + stmt.execute("CREATE TABLE AccountGroupInvites (group_name GroupName, inviter QoraAddress, invitee QoraAddress, " + + "expiry TIMESTAMP WITH TIME ZONE NOT NULL, reference Signature, PRIMARY KEY (invitee, group_name, inviter))"); // For finding invites sent by inviter - stmt.execute("CREATE INDEX AccountGroupSentInviteIndex on AccountGroupInvites (inviter)"); + stmt.execute("CREATE INDEX AccountGroupSentInviteIndex ON AccountGroupInvites (inviter)"); // For finding invites by group - stmt.execute("CREATE INDEX AccountGroupInviteIndex on AccountGroupInvites (group_name)"); + stmt.execute("CREATE INDEX AccountGroupInviteIndex ON AccountGroupInvites (group_name)"); + // For expiry maintenance + stmt.execute("CREATE INDEX AccountGroupInviteExpiryIndex ON AccountGroupInvites (expiry)"); + + // Pending "join requests" + stmt.execute("CREATE TABLE AccountGroupJoinRequests (group_name GroupName, joiner QoraAddress, " + + "PRIMARY KEY (group_name, joiner))"); // Bans // NULL expiry means does not expire! stmt.execute("CREATE TABLE AccountGroupBans (group_name GroupName, offender QoraAddress, admin QoraAddress NOT NULL, banned TIMESTAMP WITH TIME ZONE NOT NULL, " + "reason GenericDescription NOT NULL, expiry TIMESTAMP WITH TIME ZONE, PRIMARY KEY (group_name, offender))"); // For expiry maintenance - stmt.execute("CREATE INDEX AccountGroupBanExpiryIndex on AccountGroupBans (expiry)"); + stmt.execute("CREATE INDEX AccountGroupBanExpiryIndex ON AccountGroupBans (expiry)"); break; - case 31: + case 29: // Account group transactions stmt.execute("CREATE TABLE CreateGroupTransactions (signature Signature, creator QoraPublicKey NOT NULL, group_name GroupName NOT NULL, " + "owner QoraAddress NOT NULL, description GenericDescription NOT NULL, is_open BOOLEAN NOT NULL, " @@ -464,6 +455,21 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE TABLE LeaveGroupTransactions (signature Signature, leaver QoraPublicKey NOT NULL, group_name GroupName NOT NULL, " + "member_reference Signature, admin_reference Signature, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + // Account group kick transaction + stmt.execute("CREATE TABLE GroupKickTransactions (signature Signature, admin QoraPublicKey NOT NULL, group_name GroupName NOT NULL, address QoraAddress NOT NULL, " + + "reason VARCHAR(400), member_reference Signature, admin_reference Signature, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + // Account group invite/cancel-invite transactions + stmt.execute("CREATE TABLE GroupInviteTransactions (signature Signature, admin QoraPublicKey NOT NULL, group_name GroupName NOT NULL, invitee QoraAddress NOT NULL, " + + "time_to_live INTEGER NOT NULL, group_reference Signature, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // For finding invite transactions during orphaning + stmt.execute("CREATE INDEX GroupInviteTransactionReferenceIndex ON GroupInviteTransactions (group_reference)"); + // Cancel group invite + stmt.execute("CREATE TABLE CancelGroupInviteTransactions (signature Signature, admin QoraPublicKey NOT NULL, group_name GroupName NOT NULL, invitee QoraAddress NOT NULL, " + + "group_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); break; default: diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java index 1683fb39..2f0367d7 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java @@ -9,6 +9,8 @@ import java.util.List; import org.qora.data.group.GroupAdminData; import org.qora.data.group.GroupData; +import org.qora.data.group.GroupInviteData; +import org.qora.data.group.GroupJoinRequestData; import org.qora.data.group.GroupMemberData; import org.qora.repository.DataException; import org.qora.repository.GroupRepository; @@ -181,7 +183,7 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public List getAllGroupAdmins(String groupName) throws DataException { + public List getGroupAdmins(String groupName) throws DataException { List admins = new ArrayList<>(); try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin, group_reference FROM AccountGroupAdmins WHERE group_name = ?", groupName)) { @@ -253,7 +255,7 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public List getAllGroupMembers(String groupName) throws DataException { + public List getGroupMembers(String groupName) throws DataException { List members = new ArrayList<>(); try (ResultSet resultSet = this.repository.checkedExecute("SELECT address, joined, group_reference FROM AccountGroupMembers WHERE group_name = ?", @@ -311,4 +313,178 @@ public class HSQLDBGroupRepository implements GroupRepository { } } + // Group Invites + + @Override + public GroupInviteData getInvite(String groupName, String inviter, String invitee) throws DataException { + try (ResultSet resultSet = this.repository.checkedExecute("SELECT expiry, reference FROM AccountGroupInvites WHERE group_name = ?", + groupName)) { + if (resultSet == null) + return null; + + Timestamp expiryTimestamp = resultSet.getTimestamp(1, Calendar.getInstance(HSQLDBRepository.UTC)); + Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + + byte[] reference = resultSet.getBytes(2); + + return new GroupInviteData(groupName, inviter, invitee, expiry, reference); + } catch (SQLException e) { + throw new DataException("Unable to fetch group invite from repository", e); + } + } + + @Override + public boolean hasInvite(String groupName, String invitee) throws DataException { + try { + return this.repository.exists("AccountGroupInvites", "group_name = ? AND invitee = ?", groupName, invitee); + } catch (SQLException e) { + throw new DataException("Unable to check for group invite in repository", e); + } + } + + @Override + public boolean inviteExists(String groupName, String inviter, String invitee) throws DataException { + try { + return this.repository.exists("AccountGroupInvites", "group_name = ? AND inviter = ? AND invitee = ?", groupName, inviter, invitee); + } catch (SQLException e) { + throw new DataException("Unable to check for group invite in repository", e); + } + } + + @Override + public List getGroupInvites(String groupName) throws DataException { + List invites = new ArrayList<>(); + + try (ResultSet resultSet = this.repository.checkedExecute("SELECT inviter, invitee, expiry, reference FROM AccountGroupInvites WHERE group_name = ?", + groupName)) { + if (resultSet == null) + return invites; + + do { + String inviter = resultSet.getString(1); + String invitee = resultSet.getString(2); + + Timestamp expiryTimestamp = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)); + Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + + byte[] reference = resultSet.getBytes(4); + + invites.add(new GroupInviteData(groupName, inviter, invitee, expiry, reference)); + } while (resultSet.next()); + + return invites; + } catch (SQLException e) { + throw new DataException("Unable to fetch group invites from repository", e); + } + } + + @Override + public List getInvitesByInvitee(String groupName, String invitee) throws DataException { + List invites = new ArrayList<>(); + + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT inviter, expiry, reference FROM AccountGroupInvites WHERE group_name = ? AND invitee = ?", groupName, invitee)) { + if (resultSet == null) + return invites; + + do { + String inviter = resultSet.getString(1); + + Timestamp expiryTimestamp = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)); + Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + + byte[] reference = resultSet.getBytes(3); + + invites.add(new GroupInviteData(groupName, inviter, invitee, expiry, reference)); + } while (resultSet.next()); + + return invites; + } catch (SQLException e) { + throw new DataException("Unable to fetch group invites from repository", e); + } + } + + @Override + public void save(GroupInviteData groupInviteData) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("AccountGroupInvites"); + + Timestamp expiryTimestamp; + if (groupInviteData.getExpiry() == null) + expiryTimestamp = null; + else + expiryTimestamp = new Timestamp(groupInviteData.getExpiry()); + + saveHelper.bind("group_name", groupInviteData.getGroupName()).bind("inviter", groupInviteData.getInviter()) + .bind("invitee", groupInviteData.getInvitee()).bind("expiry", expiryTimestamp).bind("reference", groupInviteData.getReference()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save group invite into repository", e); + } + } + + @Override + public void deleteInvite(String groupName, String inviter, String invitee) throws DataException { + try { + this.repository.delete("AccountGroupInvites", "group_name = ? AND inviter = ? AND invitee = ?", groupName, inviter, invitee); + } catch (SQLException e) { + throw new DataException("Unable to delete group invite from repository", e); + } + } + + // Group Join Requests + + @Override + public boolean joinRequestExists(String groupName, String joiner) throws DataException { + try { + return this.repository.exists("AccountGroupJoinRequests", "group_name = ? AND joiner = ?", groupName, joiner); + } catch (SQLException e) { + throw new DataException("Unable to check for group join request in repository", e); + } + } + + @Override + public List getGroupJoinRequests(String groupName) throws DataException { + List joinRequests = new ArrayList<>(); + + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT joiner FROM AccountGroupJoinRequests WHERE group_name = ?", groupName)) { + if (resultSet == null) + return joinRequests; + + do { + String joiner = resultSet.getString(1); + + joinRequests.add(new GroupJoinRequestData(groupName, joiner)); + } while (resultSet.next()); + + return joinRequests; + } catch (SQLException e) { + throw new DataException("Unable to fetch group join requests from repository", e); + } + } + + @Override + public void save(GroupJoinRequestData groupJoinRequestData) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("AccountGroupJoinRequests"); + + saveHelper.bind("group_name", groupJoinRequestData.getGroupName()).bind("joiner", groupJoinRequestData.getJoiner()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save group join request into repository", e); + } + } + + @Override + public void deleteJoinRequest(String groupName, String joiner) throws DataException { + try { + this.repository.delete("AccountGroupJoinRequests", "group_name = ? AND joiner = ?", groupName, joiner); + } catch (SQLException e) { + throw new DataException("Unable to delete group join request from repository", e); + } + } + } diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCancelGroupInviteTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCancelGroupInviteTransactionRepository.java new file mode 100644 index 00000000..aeae1ef4 --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCancelGroupInviteTransactionRepository.java @@ -0,0 +1,50 @@ +package org.qora.repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.qora.data.transaction.CancelGroupInviteTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.repository.DataException; +import org.qora.repository.hsqldb.HSQLDBRepository; +import org.qora.repository.hsqldb.HSQLDBSaver; + +public class HSQLDBCancelGroupInviteTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBCancelGroupInviteTransactionRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException { + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT group_name, invitee FROM CancelGroupInviteTransactions WHERE signature = ?", signature)) { + if (resultSet == null) + return null; + + String groupName = resultSet.getString(1); + String invitee = resultSet.getString(2); + + return new CancelGroupInviteTransactionData(creatorPublicKey, groupName, invitee, fee, timestamp, reference, signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch cancel group invite transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + CancelGroupInviteTransactionData cancelGroupInviteTransactionData = (CancelGroupInviteTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("CancelGroupInviteTransactions"); + + saveHelper.bind("signature", cancelGroupInviteTransactionData.getSignature()).bind("admin", cancelGroupInviteTransactionData.getAdminPublicKey()) + .bind("group_name", cancelGroupInviteTransactionData.getGroupName()).bind("invitee", cancelGroupInviteTransactionData.getInvitee()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save cancel group invite transaction into repository", e); + } + } + +} diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBGroupInviteTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBGroupInviteTransactionRepository.java new file mode 100644 index 00000000..139ab603 --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBGroupInviteTransactionRepository.java @@ -0,0 +1,76 @@ +package org.qora.repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.qora.data.transaction.GroupInviteTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.repository.DataException; +import org.qora.repository.hsqldb.HSQLDBRepository; +import org.qora.repository.hsqldb.HSQLDBSaver; + +public class HSQLDBGroupInviteTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBGroupInviteTransactionRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException { + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT group_name, invitee, time_to_live, group_reference FROM GroupInviteTransactions WHERE signature = ?", signature)) { + if (resultSet == null) + return null; + + String groupName = resultSet.getString(1); + String invitee = resultSet.getString(2); + int timeToLive = resultSet.getInt(3); + byte[] groupReference = resultSet.getBytes(4); + + return new GroupInviteTransactionData(creatorPublicKey, groupName, invitee, timeToLive, groupReference, fee, timestamp, reference, signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch group invite transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + GroupInviteTransactionData groupInviteTransactionData = (GroupInviteTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("GroupInviteTransactions"); + + saveHelper.bind("signature", groupInviteTransactionData.getSignature()).bind("admin", groupInviteTransactionData.getAdminPublicKey()) + .bind("group_name", groupInviteTransactionData.getGroupName()).bind("invitee", groupInviteTransactionData.getInvitee()) + .bind("time_to_live", groupInviteTransactionData.getTimeToLive()).bind("group_reference", groupInviteTransactionData.getGroupReference()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save group invite transaction into repository", e); + } + } + + @Override + public List getInvitesWithGroupReference(byte[] groupReference) throws DataException { + List invites = new ArrayList<>(); + + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT signature FROM GroupInviteTransactions WHERE group_reference = ?", groupReference)) { + if (resultSet == null) + return invites; + + do { + byte[] signature = resultSet.getBytes(1); + + invites.add((GroupInviteTransactionData) this.repository.getTransactionRepository().fromSignature(signature)); + } while (resultSet.next()); + + return invites; + } catch (SQLException e) { + throw new DataException("Unable to fetch group invite transaction from repository", e); + } + } + +} diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBGroupKickTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBGroupKickTransactionRepository.java new file mode 100644 index 00000000..d2cf26bb --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBGroupKickTransactionRepository.java @@ -0,0 +1,56 @@ +package org.qora.repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.qora.data.transaction.GroupKickTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.repository.DataException; +import org.qora.repository.hsqldb.HSQLDBRepository; +import org.qora.repository.hsqldb.HSQLDBSaver; + +public class HSQLDBGroupKickTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBGroupKickTransactionRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException { + try (ResultSet resultSet = this.repository.checkedExecute( + "SELECT group_name, address, reason, member_reference, admin_reference FROM GroupKickTransactions WHERE signature = ?", signature)) { + if (resultSet == null) + return null; + + String groupName = resultSet.getString(1); + String member = resultSet.getString(2); + String reason = resultSet.getString(3); + byte[] memberReference = resultSet.getBytes(4); + byte[] adminReference = resultSet.getBytes(5); + + return new GroupKickTransactionData(creatorPublicKey, groupName, member, reason, memberReference, adminReference, fee, timestamp, reference, + signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch group kick transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + GroupKickTransactionData groupKickTransactionData = (GroupKickTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("GroupKickTransactions"); + + saveHelper.bind("signature", groupKickTransactionData.getSignature()).bind("admin", groupKickTransactionData.getAdminPublicKey()) + .bind("group_name", groupKickTransactionData.getGroupName()).bind("address", groupKickTransactionData.getMember()) + .bind("reason", groupKickTransactionData.getReason()).bind("member_reference", groupKickTransactionData.getMemberReference()) + .bind("admin_reference", groupKickTransactionData.getAdminReference()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save group kick transaction into repository", e); + } + } + +} diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index aa9dc7c9..74f13ea8 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -11,6 +11,7 @@ import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qora.data.PaymentData; +import org.qora.data.transaction.GroupInviteTransactionData; import org.qora.data.transaction.TransactionData; import org.qora.repository.DataException; import org.qora.repository.TransactionRepository; @@ -45,6 +46,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository { private HSQLDBUpdateGroupTransactionRepository updateGroupTransactionRepository; private HSQLDBAddGroupAdminTransactionRepository addGroupAdminTransactionRepository; private HSQLDBRemoveGroupAdminTransactionRepository removeGroupAdminTransactionRepository; + private HSQLDBGroupKickTransactionRepository groupKickTransactionRepository; + private HSQLDBGroupInviteTransactionRepository groupInviteTransactionRepository; + private HSQLDBCancelGroupInviteTransactionRepository cancelGroupInviteTransactionRepository; private HSQLDBJoinGroupTransactionRepository joinGroupTransactionRepository; private HSQLDBLeaveGroupTransactionRepository leaveGroupTransactionRepository; @@ -72,6 +76,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.updateGroupTransactionRepository = new HSQLDBUpdateGroupTransactionRepository(repository); this.addGroupAdminTransactionRepository = new HSQLDBAddGroupAdminTransactionRepository(repository); this.removeGroupAdminTransactionRepository = new HSQLDBRemoveGroupAdminTransactionRepository(repository); + this.groupKickTransactionRepository = new HSQLDBGroupKickTransactionRepository(repository); + this.groupInviteTransactionRepository = new HSQLDBGroupInviteTransactionRepository(repository); + this.cancelGroupInviteTransactionRepository = new HSQLDBCancelGroupInviteTransactionRepository(repository); this.joinGroupTransactionRepository = new HSQLDBJoinGroupTransactionRepository(repository); this.leaveGroupTransactionRepository = new HSQLDBLeaveGroupTransactionRepository(repository); } @@ -212,6 +219,15 @@ public class HSQLDBTransactionRepository implements TransactionRepository { case REMOVE_GROUP_ADMIN: return this.removeGroupAdminTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case GROUP_KICK: + return this.groupKickTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + + case GROUP_INVITE: + return this.groupInviteTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + + case CANCEL_GROUP_INVITE: + return this.cancelGroupInviteTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case JOIN_GROUP: return this.joinGroupTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); @@ -554,6 +570,18 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.removeGroupAdminTransactionRepository.save(transactionData); break; + case GROUP_KICK: + this.groupKickTransactionRepository.save(transactionData); + break; + + case GROUP_INVITE: + this.groupInviteTransactionRepository.save(transactionData); + break; + + case CANCEL_GROUP_INVITE: + this.cancelGroupInviteTransactionRepository.save(transactionData); + break; + case JOIN_GROUP: this.joinGroupTransactionRepository.save(transactionData); break; @@ -583,4 +611,10 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } } + @Override + public List getInvitesWithGroupReference(byte[] groupReference) throws DataException { + // Let specialized subclass handle this + return this.groupInviteTransactionRepository.getInvitesWithGroupReference(groupReference); + } + } diff --git a/src/main/java/org/qora/transaction/AddGroupAdminTransaction.java b/src/main/java/org/qora/transaction/AddGroupAdminTransaction.java index 60ef8179..bfe620f8 100644 --- a/src/main/java/org/qora/transaction/AddGroupAdminTransaction.java +++ b/src/main/java/org/qora/transaction/AddGroupAdminTransaction.java @@ -8,6 +8,7 @@ import java.util.List; import org.qora.account.Account; import org.qora.account.PublicKeyAccount; import org.qora.asset.Asset; +import org.qora.crypto.Crypto; import org.qora.data.transaction.AddGroupAdminTransactionData; import org.qora.data.group.GroupData; import org.qora.data.transaction.TransactionData; @@ -96,12 +97,16 @@ public class AddGroupAdminTransaction extends Transaction { if (!owner.getAddress().equals(groupData.getOwner())) return ValidationResult.INVALID_GROUP_OWNER; - // Check address is a member - if (!this.repository.getGroupRepository().memberExists(addGroupAdminTransactionData.getGroupName(), owner.getAddress())) - return ValidationResult.NOT_GROUP_MEMBER; + // Check member address is valid + if (!Crypto.isValidAddress(addGroupAdminTransactionData.getMember())) + return ValidationResult.INVALID_ADDRESS; Account member = getMember(); + // Check address is a member + if (!this.repository.getGroupRepository().memberExists(addGroupAdminTransactionData.getGroupName(), member.getAddress())) + return ValidationResult.NOT_GROUP_MEMBER; + // Check member is not already an admin if (this.repository.getGroupRepository().adminExists(addGroupAdminTransactionData.getGroupName(), member.getAddress())) return ValidationResult.ALREADY_GROUP_ADMIN; diff --git a/src/main/java/org/qora/transaction/CancelGroupInviteTransaction.java b/src/main/java/org/qora/transaction/CancelGroupInviteTransaction.java new file mode 100644 index 00000000..bd97f75d --- /dev/null +++ b/src/main/java/org/qora/transaction/CancelGroupInviteTransaction.java @@ -0,0 +1,157 @@ +package org.qora.transaction; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.qora.account.Account; +import org.qora.account.PublicKeyAccount; +import org.qora.asset.Asset; +import org.qora.crypto.Crypto; +import org.qora.data.transaction.CancelGroupInviteTransactionData; +import org.qora.data.group.GroupData; +import org.qora.data.transaction.TransactionData; +import org.qora.group.Group; +import org.qora.repository.DataException; +import org.qora.repository.GroupRepository; +import org.qora.repository.Repository; + +import com.google.common.base.Utf8; + +public class CancelGroupInviteTransaction extends Transaction { + + // Properties + private CancelGroupInviteTransactionData cancelCancelGroupInviteTransactionData; + + // Constructors + + public CancelGroupInviteTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.cancelCancelGroupInviteTransactionData = (CancelGroupInviteTransactionData) this.transactionData; + } + + // More information + + @Override + public List getRecipientAccounts() throws DataException { + return Collections.emptyList(); + } + + @Override + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getAdmin().getAddress())) + return true; + + if (address.equals(this.getInvitee().getAddress())) + return true; + + return false; + } + + @Override + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + if (address.equals(this.getAdmin().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; + } + + // Navigation + + public Account getAdmin() throws DataException { + return new PublicKeyAccount(this.repository, this.cancelCancelGroupInviteTransactionData.getAdminPublicKey()); + } + + public Account getInvitee() throws DataException { + return new Account(this.repository, this.cancelCancelGroupInviteTransactionData.getInvitee()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = cancelCancelGroupInviteTransactionData.getGroupName(); + + // Check member address is valid + if (!Crypto.isValidAddress(cancelCancelGroupInviteTransactionData.getInvitee())) + return ValidationResult.INVALID_ADDRESS; + + // Check group name size bounds + int groupNameLength = Utf8.encodedLength(groupName); + if (groupNameLength < 1 || groupNameLength > Group.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check group name is lowercase + if (!groupName.equals(groupName.toLowerCase())) + return ValidationResult.NAME_NOT_LOWER_CASE; + + GroupData groupData = groupRepository.fromGroupName(groupName); + + // Check group exists + if (groupData == null) + return ValidationResult.GROUP_DOES_NOT_EXIST; + + Account admin = getAdmin(); + Account invitee = getInvitee(); + + // Check invite exists + if (!groupRepository.inviteExists(groupName, admin.getAddress(), invitee.getAddress())) + return ValidationResult.INVITE_UNKNOWN; + + // Check fee is positive + if (cancelCancelGroupInviteTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + if (!Arrays.equals(admin.getLastReference(), cancelCancelGroupInviteTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check creator has enough funds + if (admin.getConfirmedBalance(Asset.QORA).compareTo(cancelCancelGroupInviteTransactionData.getFee()) < 0) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + } + + @Override + public void process() throws DataException { + // Update Group Membership + Group group = new Group(this.repository, cancelCancelGroupInviteTransactionData.getGroupName()); + group.cancelInvite(cancelCancelGroupInviteTransactionData); + + // Save this transaction with updated member/admin references to transactions that can help restore state + this.repository.getTransactionRepository().save(cancelCancelGroupInviteTransactionData); + + // Update admin's balance + Account admin = getAdmin(); + admin.setConfirmedBalance(Asset.QORA, admin.getConfirmedBalance(Asset.QORA).subtract(cancelCancelGroupInviteTransactionData.getFee())); + + // Update admin's reference + admin.setLastReference(cancelCancelGroupInviteTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Revert group membership + Group group = new Group(this.repository, cancelCancelGroupInviteTransactionData.getGroupName()); + group.uncancelInvite(cancelCancelGroupInviteTransactionData); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(cancelCancelGroupInviteTransactionData); + + // Update admin's balance + Account admin = getAdmin(); + admin.setConfirmedBalance(Asset.QORA, admin.getConfirmedBalance(Asset.QORA).add(cancelCancelGroupInviteTransactionData.getFee())); + + // Update admin's reference + admin.setLastReference(cancelCancelGroupInviteTransactionData.getReference()); + } + +} diff --git a/src/main/java/org/qora/transaction/GroupInviteTransaction.java b/src/main/java/org/qora/transaction/GroupInviteTransaction.java new file mode 100644 index 00000000..f2ff1b55 --- /dev/null +++ b/src/main/java/org/qora/transaction/GroupInviteTransaction.java @@ -0,0 +1,165 @@ +package org.qora.transaction; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.qora.account.Account; +import org.qora.account.PublicKeyAccount; +import org.qora.asset.Asset; +import org.qora.crypto.Crypto; +import org.qora.data.transaction.GroupInviteTransactionData; +import org.qora.data.group.GroupData; +import org.qora.data.transaction.TransactionData; +import org.qora.group.Group; +import org.qora.repository.DataException; +import org.qora.repository.GroupRepository; +import org.qora.repository.Repository; + +import com.google.common.base.Utf8; + +public class GroupInviteTransaction extends Transaction { + + // Properties + private GroupInviteTransactionData groupInviteTransactionData; + + // Constructors + + public GroupInviteTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.groupInviteTransactionData = (GroupInviteTransactionData) this.transactionData; + } + + // More information + + @Override + public List getRecipientAccounts() throws DataException { + return Collections.emptyList(); + } + + @Override + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getAdmin().getAddress())) + return true; + + if (address.equals(this.getInvitee().getAddress())) + return true; + + return false; + } + + @Override + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + if (address.equals(this.getAdmin().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; + } + + // Navigation + + public Account getAdmin() throws DataException { + return new PublicKeyAccount(this.repository, this.groupInviteTransactionData.getAdminPublicKey()); + } + + public Account getInvitee() throws DataException { + return new Account(this.repository, this.groupInviteTransactionData.getInvitee()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = groupInviteTransactionData.getGroupName(); + + // Check time to live zero (infinite) or positive + if (groupInviteTransactionData.getTimeToLive() < 0) + return ValidationResult.INVALID_LIFETIME; + + // Check member address is valid + if (!Crypto.isValidAddress(groupInviteTransactionData.getInvitee())) + return ValidationResult.INVALID_ADDRESS; + + // Check group name size bounds + int groupNameLength = Utf8.encodedLength(groupName); + if (groupNameLength < 1 || groupNameLength > Group.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check group name is lowercase + if (!groupName.equals(groupName.toLowerCase())) + return ValidationResult.NAME_NOT_LOWER_CASE; + + GroupData groupData = groupRepository.fromGroupName(groupName); + + // Check group exists + if (groupData == null) + return ValidationResult.GROUP_DOES_NOT_EXIST; + + Account admin = getAdmin(); + Account invitee = getInvitee(); + + // Can't invite if not an admin + if (!groupRepository.adminExists(groupName, admin.getAddress())) + return ValidationResult.NOT_GROUP_ADMIN; + + // Check invitee not already in group + if (groupRepository.memberExists(groupName, invitee.getAddress())) + return ValidationResult.ALREADY_GROUP_MEMBER; + + // Check fee is positive + if (groupInviteTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + if (!Arrays.equals(admin.getLastReference(), groupInviteTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check creator has enough funds + if (admin.getConfirmedBalance(Asset.QORA).compareTo(groupInviteTransactionData.getFee()) < 0) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + } + + @Override + public void process() throws DataException { + // Update Group Membership + Group group = new Group(this.repository, groupInviteTransactionData.getGroupName()); + group.invite(groupInviteTransactionData); + + // Save this transaction with updated member/admin references to transactions that can help restore state + this.repository.getTransactionRepository().save(groupInviteTransactionData); + + // Update admin's balance + Account admin = getAdmin(); + admin.setConfirmedBalance(Asset.QORA, admin.getConfirmedBalance(Asset.QORA).subtract(groupInviteTransactionData.getFee())); + + // Update admin's reference + admin.setLastReference(groupInviteTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Revert group membership + Group group = new Group(this.repository, groupInviteTransactionData.getGroupName()); + group.uninvite(groupInviteTransactionData); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(groupInviteTransactionData); + + // Update admin's balance + Account admin = getAdmin(); + admin.setConfirmedBalance(Asset.QORA, admin.getConfirmedBalance(Asset.QORA).add(groupInviteTransactionData.getFee())); + + // Update admin's reference + admin.setLastReference(groupInviteTransactionData.getReference()); + } + +} diff --git a/src/main/java/org/qora/transaction/GroupKickTransaction.java b/src/main/java/org/qora/transaction/GroupKickTransaction.java new file mode 100644 index 00000000..2517b79d --- /dev/null +++ b/src/main/java/org/qora/transaction/GroupKickTransaction.java @@ -0,0 +1,165 @@ +package org.qora.transaction; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.qora.account.Account; +import org.qora.account.PublicKeyAccount; +import org.qora.asset.Asset; +import org.qora.crypto.Crypto; +import org.qora.data.transaction.GroupKickTransactionData; +import org.qora.data.group.GroupData; +import org.qora.data.transaction.TransactionData; +import org.qora.group.Group; +import org.qora.repository.DataException; +import org.qora.repository.GroupRepository; +import org.qora.repository.Repository; + +import com.google.common.base.Utf8; + +public class GroupKickTransaction extends Transaction { + + // Properties + private GroupKickTransactionData groupKickTransactionData; + + // Constructors + + public GroupKickTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.groupKickTransactionData = (GroupKickTransactionData) this.transactionData; + } + + // More information + + @Override + public List getRecipientAccounts() throws DataException { + return Collections.emptyList(); + } + + @Override + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getAdmin().getAddress())) + return true; + + if (address.equals(this.getMember().getAddress())) + return true; + + return false; + } + + @Override + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + if (address.equals(this.getAdmin().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; + } + + // Navigation + + public Account getAdmin() throws DataException { + return new PublicKeyAccount(this.repository, this.groupKickTransactionData.getAdminPublicKey()); + } + + public Account getMember() throws DataException { + return new Account(this.repository, this.groupKickTransactionData.getMember()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = groupKickTransactionData.getGroupName(); + + // Check member address is valid + if (!Crypto.isValidAddress(groupKickTransactionData.getMember())) + return ValidationResult.INVALID_ADDRESS; + + // Check group name size bounds + int groupNameLength = Utf8.encodedLength(groupName); + if (groupNameLength < 1 || groupNameLength > Group.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check group name is lowercase + if (!groupName.equals(groupName.toLowerCase())) + return ValidationResult.NAME_NOT_LOWER_CASE; + + GroupData groupData = groupRepository.fromGroupName(groupName); + + // Check group exists + if (groupData == null) + return ValidationResult.GROUP_DOES_NOT_EXIST; + + Account admin = getAdmin(); + Account member = getMember(); + + // Can't kick if not an admin + if (!groupRepository.adminExists(groupName, admin.getAddress())) + return ValidationResult.NOT_GROUP_ADMIN; + + // Check member actually in group + if (!groupRepository.memberExists(groupName, member.getAddress())) + return ValidationResult.NOT_GROUP_MEMBER; + + // Can't kick another admin unless the group owner + if (!admin.getAddress().equals(groupData.getOwner()) && groupRepository.adminExists(groupName, member.getAddress())) + return ValidationResult.INVALID_GROUP_OWNER; + + // Check fee is positive + if (groupKickTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + if (!Arrays.equals(admin.getLastReference(), groupKickTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check creator has enough funds + if (admin.getConfirmedBalance(Asset.QORA).compareTo(groupKickTransactionData.getFee()) < 0) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + } + + @Override + public void process() throws DataException { + // Update Group Membership + Group group = new Group(this.repository, groupKickTransactionData.getGroupName()); + group.kick(groupKickTransactionData); + + // Save this transaction with updated member/admin references to transactions that can help restore state + this.repository.getTransactionRepository().save(groupKickTransactionData); + + // Update admin's balance + Account admin = getAdmin(); + admin.setConfirmedBalance(Asset.QORA, admin.getConfirmedBalance(Asset.QORA).subtract(groupKickTransactionData.getFee())); + + // Update admin's reference + admin.setLastReference(groupKickTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Revert group membership + Group group = new Group(this.repository, groupKickTransactionData.getGroupName()); + group.unkick(groupKickTransactionData); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(groupKickTransactionData); + + // Update admin's balance + Account admin = getAdmin(); + admin.setConfirmedBalance(Asset.QORA, admin.getConfirmedBalance(Asset.QORA).add(groupKickTransactionData.getFee())); + + // Update admin's reference + admin.setLastReference(groupKickTransactionData.getReference()); + } + +} diff --git a/src/main/java/org/qora/transaction/JoinGroupTransaction.java b/src/main/java/org/qora/transaction/JoinGroupTransaction.java index e4fbad14..25dc71fa 100644 --- a/src/main/java/org/qora/transaction/JoinGroupTransaction.java +++ b/src/main/java/org/qora/transaction/JoinGroupTransaction.java @@ -88,6 +88,8 @@ public class JoinGroupTransaction extends Transaction { if (this.repository.getGroupRepository().memberExists(joinGroupTransactionData.getGroupName(), joiner.getAddress())) return ValidationResult.ALREADY_GROUP_MEMBER; + // XXX Check member is not banned + // Check fee is positive if (joinGroupTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) return ValidationResult.NEGATIVE_FEE; diff --git a/src/main/java/org/qora/transaction/RemoveGroupAdminTransaction.java b/src/main/java/org/qora/transaction/RemoveGroupAdminTransaction.java index 5ec6da22..abc8501f 100644 --- a/src/main/java/org/qora/transaction/RemoveGroupAdminTransaction.java +++ b/src/main/java/org/qora/transaction/RemoveGroupAdminTransaction.java @@ -8,6 +8,7 @@ import java.util.List; import org.qora.account.Account; import org.qora.account.PublicKeyAccount; import org.qora.asset.Asset; +import org.qora.crypto.Crypto; import org.qora.data.transaction.RemoveGroupAdminTransactionData; import org.qora.data.group.GroupData; import org.qora.data.transaction.TransactionData; @@ -75,6 +76,10 @@ public class RemoveGroupAdminTransaction extends Transaction { @Override public ValidationResult isValid() throws DataException { + // Check admin address is valid + if (!Crypto.isValidAddress(removeGroupAdminTransactionData.getAdmin())) + return ValidationResult.INVALID_ADDRESS; + // Check group name size bounds int groupNameLength = Utf8.encodedLength(removeGroupAdminTransactionData.getGroupName()); if (groupNameLength < 1 || groupNameLength > Group.MAX_NAME_SIZE) diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java index f39cbc7d..0d383797 100644 --- a/src/main/java/org/qora/transaction/Transaction.java +++ b/src/main/java/org/qora/transaction/Transaction.java @@ -133,6 +133,8 @@ public abstract class Transaction { NOT_GROUP_MEMBER(53), ALREADY_GROUP_ADMIN(54), NOT_GROUP_ADMIN(55), + INVALID_LIFETIME(56), + INVITE_UNKNOWN(57), NOT_YET_RELEASED(1000); public final int value; @@ -244,6 +246,15 @@ public abstract class Transaction { case REMOVE_GROUP_ADMIN: return new RemoveGroupAdminTransaction(repository, transactionData); + case GROUP_KICK: + return new GroupKickTransaction(repository, transactionData); + + case GROUP_INVITE: + return new GroupInviteTransaction(repository, transactionData); + + case CANCEL_GROUP_INVITE: + return new CancelGroupInviteTransaction(repository, transactionData); + case JOIN_GROUP: return new JoinGroupTransaction(repository, transactionData); diff --git a/src/main/java/org/qora/transform/transaction/CancelGroupInviteTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/CancelGroupInviteTransactionTransformer.java new file mode 100644 index 00000000..0ff0c7fb --- /dev/null +++ b/src/main/java/org/qora/transform/transaction/CancelGroupInviteTransactionTransformer.java @@ -0,0 +1,104 @@ +package org.qora.transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; + +import org.json.simple.JSONObject; +import org.qora.account.PublicKeyAccount; +import org.qora.data.transaction.CancelGroupInviteTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.group.Group; +import org.qora.transform.TransformationException; +import org.qora.utils.Serialization; + +import com.google.common.base.Utf8; +import com.google.common.hash.HashCode; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +public class CancelGroupInviteTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int ADMIN_LENGTH = PUBLIC_KEY_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + private static final int INVITEE_LENGTH = ADDRESS_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + ADMIN_LENGTH + NAME_SIZE_LENGTH + INVITEE_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] adminPublicKey = Serialization.deserializePublicKey(byteBuffer); + + String groupName = Serialization.deserializeSizedString(byteBuffer, Group.MAX_NAME_SIZE); + + String invitee = Serialization.deserializeAddress(byteBuffer); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new CancelGroupInviteTransactionData(adminPublicKey, groupName, invitee, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + CancelGroupInviteTransactionData cancelGroupInviteTransactionData = (CancelGroupInviteTransactionData) transactionData; + + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(cancelGroupInviteTransactionData.getGroupName()); + + return dataLength; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + CancelGroupInviteTransactionData cancelGroupInviteTransactionData = (CancelGroupInviteTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(cancelGroupInviteTransactionData.getType().value)); + bytes.write(Longs.toByteArray(cancelGroupInviteTransactionData.getTimestamp())); + bytes.write(cancelGroupInviteTransactionData.getReference()); + + bytes.write(cancelGroupInviteTransactionData.getCreatorPublicKey()); + Serialization.serializeSizedString(bytes, cancelGroupInviteTransactionData.getGroupName()); + Serialization.serializeAddress(bytes, cancelGroupInviteTransactionData.getInvitee()); + + Serialization.serializeBigDecimal(bytes, cancelGroupInviteTransactionData.getFee()); + + if (cancelGroupInviteTransactionData.getSignature() != null) + bytes.write(cancelGroupInviteTransactionData.getSignature()); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + + @SuppressWarnings("unchecked") + public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { + JSONObject json = TransactionTransformer.getBaseJSON(transactionData); + + try { + CancelGroupInviteTransactionData cancelGroupInviteTransactionData = (CancelGroupInviteTransactionData) transactionData; + + byte[] adminPublicKey = cancelGroupInviteTransactionData.getAdminPublicKey(); + + json.put("admin", PublicKeyAccount.getAddress(adminPublicKey)); + json.put("adminPublicKey", HashCode.fromBytes(adminPublicKey).toString()); + + json.put("groupName", cancelGroupInviteTransactionData.getGroupName()); + json.put("invitee", cancelGroupInviteTransactionData.getInvitee()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} \ No newline at end of file diff --git a/src/main/java/org/qora/transform/transaction/GroupInviteTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/GroupInviteTransactionTransformer.java new file mode 100644 index 00000000..62172423 --- /dev/null +++ b/src/main/java/org/qora/transform/transaction/GroupInviteTransactionTransformer.java @@ -0,0 +1,109 @@ +package org.qora.transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; + +import org.json.simple.JSONObject; +import org.qora.account.PublicKeyAccount; +import org.qora.data.transaction.GroupInviteTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.group.Group; +import org.qora.transform.TransformationException; +import org.qora.utils.Serialization; + +import com.google.common.base.Utf8; +import com.google.common.hash.HashCode; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +public class GroupInviteTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int ADMIN_LENGTH = PUBLIC_KEY_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + private static final int INVITEE_LENGTH = ADDRESS_LENGTH; + private static final int TTL_LENGTH = INT_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + ADMIN_LENGTH + NAME_SIZE_LENGTH + INVITEE_LENGTH + TTL_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] adminPublicKey = Serialization.deserializePublicKey(byteBuffer); + + String groupName = Serialization.deserializeSizedString(byteBuffer, Group.MAX_NAME_SIZE); + + String invitee = Serialization.deserializeAddress(byteBuffer); + + int timeToLive = byteBuffer.getInt(); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new GroupInviteTransactionData(adminPublicKey, groupName, invitee, timeToLive, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + GroupInviteTransactionData groupInviteTransactionData = (GroupInviteTransactionData) transactionData; + + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(groupInviteTransactionData.getGroupName()); + + return dataLength; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + GroupInviteTransactionData groupInviteTransactionData = (GroupInviteTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(groupInviteTransactionData.getType().value)); + bytes.write(Longs.toByteArray(groupInviteTransactionData.getTimestamp())); + bytes.write(groupInviteTransactionData.getReference()); + + bytes.write(groupInviteTransactionData.getCreatorPublicKey()); + Serialization.serializeSizedString(bytes, groupInviteTransactionData.getGroupName()); + Serialization.serializeAddress(bytes, groupInviteTransactionData.getInvitee()); + bytes.write(Ints.toByteArray(groupInviteTransactionData.getTimeToLive())); + + Serialization.serializeBigDecimal(bytes, groupInviteTransactionData.getFee()); + + if (groupInviteTransactionData.getSignature() != null) + bytes.write(groupInviteTransactionData.getSignature()); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + + @SuppressWarnings("unchecked") + public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { + JSONObject json = TransactionTransformer.getBaseJSON(transactionData); + + try { + GroupInviteTransactionData groupInviteTransactionData = (GroupInviteTransactionData) transactionData; + + byte[] adminPublicKey = groupInviteTransactionData.getAdminPublicKey(); + + json.put("admin", PublicKeyAccount.getAddress(adminPublicKey)); + json.put("adminPublicKey", HashCode.fromBytes(adminPublicKey).toString()); + + json.put("groupName", groupInviteTransactionData.getGroupName()); + json.put("invitee", groupInviteTransactionData.getInvitee()); + json.put("timeToLive", groupInviteTransactionData.getTimeToLive()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/main/java/org/qora/transform/transaction/GroupKickTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/GroupKickTransactionTransformer.java new file mode 100644 index 00000000..bf034d2a --- /dev/null +++ b/src/main/java/org/qora/transform/transaction/GroupKickTransactionTransformer.java @@ -0,0 +1,110 @@ +package org.qora.transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; + +import org.json.simple.JSONObject; +import org.qora.account.PublicKeyAccount; +import org.qora.data.transaction.GroupKickTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.group.Group; +import org.qora.transform.TransformationException; +import org.qora.utils.Serialization; + +import com.google.common.base.Utf8; +import com.google.common.hash.HashCode; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +public class GroupKickTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int ADMIN_LENGTH = PUBLIC_KEY_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + private static final int MEMBER_LENGTH = ADDRESS_LENGTH; + private static final int REASON_SIZE_LENGTH = INT_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + ADMIN_LENGTH + NAME_SIZE_LENGTH + MEMBER_LENGTH + REASON_SIZE_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] adminPublicKey = Serialization.deserializePublicKey(byteBuffer); + + String groupName = Serialization.deserializeSizedString(byteBuffer, Group.MAX_NAME_SIZE); + + String member = Serialization.deserializeAddress(byteBuffer); + + String reason = Serialization.deserializeSizedString(byteBuffer, Group.MAX_REASON_SIZE); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new GroupKickTransactionData(adminPublicKey, groupName, member, reason, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + GroupKickTransactionData groupKickTransactionData = (GroupKickTransactionData) transactionData; + + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(groupKickTransactionData.getGroupName()) + + Utf8.encodedLength(groupKickTransactionData.getReason()); + + return dataLength; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + GroupKickTransactionData groupKickTransactionData = (GroupKickTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(groupKickTransactionData.getType().value)); + bytes.write(Longs.toByteArray(groupKickTransactionData.getTimestamp())); + bytes.write(groupKickTransactionData.getReference()); + + bytes.write(groupKickTransactionData.getCreatorPublicKey()); + Serialization.serializeSizedString(bytes, groupKickTransactionData.getGroupName()); + Serialization.serializeAddress(bytes, groupKickTransactionData.getMember()); + Serialization.serializeSizedString(bytes, groupKickTransactionData.getReason()); + + Serialization.serializeBigDecimal(bytes, groupKickTransactionData.getFee()); + + if (groupKickTransactionData.getSignature() != null) + bytes.write(groupKickTransactionData.getSignature()); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + + @SuppressWarnings("unchecked") + public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { + JSONObject json = TransactionTransformer.getBaseJSON(transactionData); + + try { + GroupKickTransactionData groupKickTransactionData = (GroupKickTransactionData) transactionData; + + byte[] adminPublicKey = groupKickTransactionData.getAdminPublicKey(); + + json.put("admin", PublicKeyAccount.getAddress(adminPublicKey)); + json.put("adminPublicKey", HashCode.fromBytes(adminPublicKey).toString()); + + json.put("groupName", groupKickTransactionData.getGroupName()); + json.put("member", groupKickTransactionData.getMember()); + json.put("reason", groupKickTransactionData.getReason()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java index 9b1d5666..eb5c3018 100644 --- a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java +++ b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java @@ -106,6 +106,15 @@ public class TransactionTransformer extends Transformer { case REMOVE_GROUP_ADMIN: return RemoveGroupAdminTransactionTransformer.fromByteBuffer(byteBuffer); + case GROUP_KICK: + return GroupKickTransactionTransformer.fromByteBuffer(byteBuffer); + + case GROUP_INVITE: + return GroupInviteTransactionTransformer.fromByteBuffer(byteBuffer); + + case CANCEL_GROUP_INVITE: + return CancelGroupInviteTransactionTransformer.fromByteBuffer(byteBuffer); + case JOIN_GROUP: return JoinGroupTransactionTransformer.fromByteBuffer(byteBuffer); @@ -185,6 +194,15 @@ public class TransactionTransformer extends Transformer { case REMOVE_GROUP_ADMIN: return RemoveGroupAdminTransactionTransformer.getDataLength(transactionData); + case GROUP_KICK: + return GroupKickTransactionTransformer.getDataLength(transactionData); + + case GROUP_INVITE: + return GroupInviteTransactionTransformer.getDataLength(transactionData); + + case CANCEL_GROUP_INVITE: + return CancelGroupInviteTransactionTransformer.getDataLength(transactionData); + case JOIN_GROUP: return JoinGroupTransactionTransformer.getDataLength(transactionData); @@ -261,6 +279,15 @@ public class TransactionTransformer extends Transformer { case REMOVE_GROUP_ADMIN: return RemoveGroupAdminTransactionTransformer.toBytes(transactionData); + case GROUP_KICK: + return GroupKickTransactionTransformer.toBytes(transactionData); + + case GROUP_INVITE: + return GroupInviteTransactionTransformer.toBytes(transactionData); + + case CANCEL_GROUP_INVITE: + return CancelGroupInviteTransactionTransformer.toBytes(transactionData); + case JOIN_GROUP: return JoinGroupTransactionTransformer.toBytes(transactionData); @@ -346,6 +373,15 @@ public class TransactionTransformer extends Transformer { case REMOVE_GROUP_ADMIN: return RemoveGroupAdminTransactionTransformer.toBytesForSigningImpl(transactionData); + case GROUP_KICK: + return GroupKickTransactionTransformer.toBytesForSigningImpl(transactionData); + + case GROUP_INVITE: + return GroupInviteTransactionTransformer.toBytesForSigningImpl(transactionData); + + case CANCEL_GROUP_INVITE: + return CancelGroupInviteTransactionTransformer.toBytesForSigningImpl(transactionData); + case JOIN_GROUP: return JoinGroupTransactionTransformer.toBytesForSigningImpl(transactionData); @@ -443,6 +479,15 @@ public class TransactionTransformer extends Transformer { case REMOVE_GROUP_ADMIN: return RemoveGroupAdminTransactionTransformer.toJSON(transactionData); + case GROUP_KICK: + return GroupKickTransactionTransformer.toJSON(transactionData); + + case GROUP_INVITE: + return GroupInviteTransactionTransformer.toJSON(transactionData); + + case CANCEL_GROUP_INVITE: + return CancelGroupInviteTransactionTransformer.toJSON(transactionData); + case JOIN_GROUP: return JoinGroupTransactionTransformer.toJSON(transactionData);