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.
This commit is contained in:
catbref 2019-01-14 14:36:43 +00:00
parent df730d9fb9
commit 90f1676c7c
27 changed files with 2066 additions and 44 deletions

View File

@ -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<GroupAdminData> groupAdmins = repository.getGroupRepository().getAllGroupAdmins(groupData.getGroupName());
List<GroupAdminData> groupAdmins = repository.getGroupRepository().getGroupAdmins(groupData.getGroupName());
// We only need admin addresses
List<String> 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<GroupInviteData> 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<GroupJoinRequestData> 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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<GroupInviteData> 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<GroupInviteTransactionData> 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();

View File

@ -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<GroupAdminData> getAllGroupAdmins(String groupName) throws DataException;
public List<GroupAdminData> 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<GroupMemberData> getAllGroupMembers(String groupName) throws DataException;
public List<GroupMemberData> 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<GroupInviteData> getGroupInvites(String groupName) throws DataException;
public List<GroupInviteData> 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<GroupJoinRequestData> getGroupJoinRequests(String groupName) throws DataException;
public void save(GroupJoinRequestData groupJoinRequestData) throws DataException;
public void deleteJoinRequest(String groupName, String joiner) throws DataException;
}

View File

@ -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<GroupInviteTransactionData> getInvitesWithGroupReference(byte[] groupReference) throws DataException;
}

View File

@ -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:

View File

@ -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<GroupAdminData> getAllGroupAdmins(String groupName) throws DataException {
public List<GroupAdminData> getGroupAdmins(String groupName) throws DataException {
List<GroupAdminData> 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<GroupMemberData> getAllGroupMembers(String groupName) throws DataException {
public List<GroupMemberData> getGroupMembers(String groupName) throws DataException {
List<GroupMemberData> 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<GroupInviteData> getGroupInvites(String groupName) throws DataException {
List<GroupInviteData> 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<GroupInviteData> getInvitesByInvitee(String groupName, String invitee) throws DataException {
List<GroupInviteData> 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<GroupJoinRequestData> getGroupJoinRequests(String groupName) throws DataException {
List<GroupJoinRequestData> 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<GroupInviteTransactionData> getInvitesWithGroupReference(byte[] groupReference) throws DataException {
List<GroupInviteTransactionData> 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<GroupInviteTransactionData> getInvitesWithGroupReference(byte[] groupReference) throws DataException {
// Let specialized subclass handle this
return this.groupInviteTransactionRepository.getInvitesWithGroupReference(groupReference);
}
}

View File

@ -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;

View File

@ -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<Account> 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());
}
}

View File

@ -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<Account> 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());
}
}

View File

@ -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<Account> 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());
}
}

View File

@ -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;

View File

@ -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)

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);