diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java index 756c8535e..0b9b48bd9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java @@ -209,6 +209,10 @@ class ConversationVisitor implements text = ctx.getString( R.string.groups_invitations_response_accepted_sent, contactName.getValue()); + } else if (r.isAutoDecline()) { + text = ctx.getString( + R.string.groups_invitations_response_declined_auto, + contactName.getValue()); } else { text = ctx.getString( R.string.groups_invitations_response_declined_sent, diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index f138d30ae..d9eec9975 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -365,6 +365,7 @@ You accepted the group invitation from %s. You declined the group invitation from %s. + The group invitation from %s was automatically declined. %s accepted the group invitation. %s declined the group invitation. Only the creator can invite new members to the group. Below are all current members of the group. diff --git a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java index 37cfef49a..8d0c9cbef 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java @@ -15,7 +15,7 @@ public class BlogInvitationResponse extends InvitationResponse { SessionId sessionId, boolean accept, GroupId shareableId, long autoDeleteTimer) { super(id, groupId, time, local, read, sent, seen, sessionId, - accept, shareableId, autoDeleteTimer); + accept, shareableId, autoDeleteTimer, false); } @Override diff --git a/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationResponse.java index 974d87c4e..19abb9ea7 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationResponse.java @@ -12,14 +12,16 @@ import javax.annotation.concurrent.Immutable; public abstract class ConversationResponse extends ConversationMessageHeader { private final SessionId sessionId; - private final boolean accepted; + private final boolean accepted, isAutoDecline; public ConversationResponse(MessageId id, GroupId groupId, long time, boolean local, boolean read, boolean sent, boolean seen, - SessionId sessionId, boolean accepted, long autoDeleteTimer) { + SessionId sessionId, boolean accepted, long autoDeleteTimer, + boolean isAutoDecline) { super(id, groupId, time, local, read, sent, seen, autoDeleteTimer); this.sessionId = sessionId; this.accepted = accepted; + this.isAutoDecline = isAutoDecline; } public SessionId getSessionId() { @@ -30,4 +32,7 @@ public abstract class ConversationResponse extends ConversationMessageHeader { return accepted; } + public boolean isAutoDecline() { + return isAutoDecline; + } } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java index be3757e35..033d7ad37 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java @@ -18,7 +18,7 @@ public class ForumInvitationResponse extends InvitationResponse { SessionId sessionId, boolean accept, GroupId shareableId, long autoDeleteTimer) { super(id, groupId, time, local, read, sent, seen, sessionId, - accept, shareableId, autoDeleteTimer); + accept, shareableId, autoDeleteTimer, false); } @Override diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java index 705b10a24..d05baadc0 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java @@ -1,13 +1,13 @@ package org.briarproject.briar.api.introduction; import org.briarproject.bramble.api.identity.Author; -import org.briarproject.briar.api.identity.AuthorInfo; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.conversation.ConversationMessageVisitor; import org.briarproject.briar.api.conversation.ConversationResponse; +import org.briarproject.briar.api.identity.AuthorInfo; import javax.annotation.concurrent.Immutable; @@ -28,7 +28,7 @@ public class IntroductionResponse extends ConversationResponse { AuthorInfo introducedAuthorInfo, Role role, boolean canSucceed, long autoDeleteTimer) { super(messageId, groupId, time, local, read, sent, seen, sessionId, - accepted, autoDeleteTimer); + accepted, autoDeleteTimer, false); this.introducedAuthor = author; this.introducedAuthorInfo = introducedAuthorInfo; this.ourRole = role; diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java index b461d9dc1..0f7d06367 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java @@ -16,9 +16,9 @@ public class GroupInvitationResponse extends InvitationResponse { public GroupInvitationResponse(MessageId id, GroupId groupId, long time, boolean local, boolean read, boolean sent, boolean seen, SessionId sessionId, boolean accept, GroupId shareableId, - long autoDeleteTimer) { + long autoDeleteTimer, boolean isAutoDecline) { super(id, groupId, time, local, read, sent, seen, sessionId, - accept, shareableId, autoDeleteTimer); + accept, shareableId, autoDeleteTimer, isAutoDecline); } @Override diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java index fd14fa051..18d08fd78 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java @@ -12,9 +12,9 @@ public abstract class InvitationResponse extends ConversationResponse { public InvitationResponse(MessageId id, GroupId groupId, long time, boolean local, boolean read, boolean sent, boolean seen, SessionId sessionId, boolean accepted, GroupId shareableId, - long autoDeleteTimer) { + long autoDeleteTimer, boolean isAutoDecline) { super(id, groupId, time, local, read, sent, seen, sessionId, accepted, - autoDeleteTimer); + autoDeleteTimer, isAutoDecline); this.shareableId = shareableId; } diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java index b52156ecd..906f4abc8 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java @@ -7,6 +7,7 @@ import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; @@ -19,13 +20,16 @@ import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.versioning.ClientVersioningManager; import org.briarproject.briar.api.autodelete.AutoDeleteManager; import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.conversation.ConversationManager; import org.briarproject.briar.api.privategroup.GroupMessage; import org.briarproject.briar.api.privategroup.GroupMessageFactory; import org.briarproject.briar.api.privategroup.PrivateGroup; import org.briarproject.briar.api.privategroup.PrivateGroupFactory; import org.briarproject.briar.api.privategroup.PrivateGroupManager; +import org.briarproject.briar.api.privategroup.event.GroupInvitationResponseReceivedEvent; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse; import java.util.Collection; @@ -132,6 +136,10 @@ abstract class AbstractProtocolEngine> privateGroup.getCreator(), privateGroup.getSalt(), text, signature, timer); sendMessage(txn, m, INVITE, privateGroup.getId(), true, timer); + // Set the auto-delete timer duration on the message + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } } else { m = messageEncoder.encodeInviteMessage(s.getContactGroupId(), privateGroup.getId(), timestamp, privateGroup.getName(), @@ -162,6 +170,10 @@ abstract class AbstractProtocolEngine> s.getLastLocalMessageId(), timer); sendMessage(txn, m, JOIN, s.getPrivateGroupId(), visibleInUi, timer); + // Set the auto-delete timer duration on the message + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } } else { m = messageEncoder.encodeJoinMessage(s.getContactGroupId(), s.getPrivateGroupId(), localTimestamp, @@ -172,15 +184,20 @@ abstract class AbstractProtocolEngine> return m; } - Message sendLeaveMessage(Transaction txn, S s, boolean visibleInUi) - throws DbException { + Message sendLeaveMessage(Transaction txn, S s) throws DbException { + return sendLeaveMessage(txn, s, false, false); + } + + Message sendLeaveMessage(Transaction txn, S s, boolean visibleInUi, + boolean isAutoDecline) throws DbException { + if (!visibleInUi && isAutoDecline) throw new IllegalArgumentException(); Message m; long localTimestamp = visibleInUi ? getTimestampForVisibleMessage(txn, s) : getTimestampForInvisibleMessage(s); ContactId c = getContactId(txn, s.getContactGroupId()); if (contactSupportsAutoDeletion(txn, c)) { - // Set auto-delete timer if manually accepting an invitation + // Set auto-delete timer if declining an invitation long timer = NO_AUTO_DELETE_TIMER; if (visibleInUi) { timer = autoDeleteManager.getAutoDeleteTimer(txn, c, @@ -190,7 +207,22 @@ abstract class AbstractProtocolEngine> s.getPrivateGroupId(), localTimestamp, s.getLastLocalMessageId(), timer); sendMessage(txn, m, LEAVE, s.getPrivateGroupId(), visibleInUi, - timer); + timer, isAutoDecline); + // Set the auto-delete timer duration on the local message + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } + if (isAutoDecline) { + // Broadcast an event, so the auto-decline becomes visible + SessionId sessionId = + new SessionId(s.getPrivateGroupId().getBytes()); + GroupInvitationResponse response = new GroupInvitationResponse( + m.getId(), s.getContactGroupId(), m.getTimestamp(), + true, true, false, false, sessionId, false, + s.getPrivateGroupId(), timer, true); + Event e = new GroupInvitationResponseReceivedEvent(response, c); + txn.attach(e); + } } else { m = messageEncoder.encodeLeaveMessage(s.getContactGroupId(), s.getPrivateGroupId(), localTimestamp, @@ -309,9 +341,17 @@ abstract class AbstractProtocolEngine> private void sendMessage(Transaction txn, Message m, MessageType type, GroupId privateGroupId, boolean visibleInConversation, long autoDeleteTimer) throws DbException { + sendMessage(txn, m, type, privateGroupId, visibleInConversation, + autoDeleteTimer, false); + } + + private void sendMessage(Transaction txn, Message m, MessageType type, + GroupId privateGroupId, boolean visibleInConversation, + long autoDeleteTimer, boolean isAutoDecline) throws DbException { BdfDictionary meta = messageEncoder.encodeMetadata(type, privateGroupId, m.getTimestamp(), true, true, - visibleInConversation, false, false, autoDeleteTimer); + visibleInConversation, false, false, autoDeleteTimer, + isAutoDecline); try { clientHelper.addLocalMessage(txn, m, meta, true, false); } catch (FormatException e) { diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java index 07ff7a802..e8d85fc34 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java @@ -84,8 +84,8 @@ class CreatorProtocolEngine extends AbstractProtocolEngine { } @Override - public CreatorSession onLeaveAction(Transaction txn, CreatorSession s) - throws DbException { + public CreatorSession onLeaveAction(Transaction txn, CreatorSession s, + boolean isAutoDecline) throws DbException { switch (s.getState()) { case START: case DISSOLVED: @@ -180,7 +180,7 @@ class CreatorProtocolEngine extends AbstractProtocolEngine { throw new DbException(e); // Invalid group metadata } // Send a LEAVE message - Message sent = sendLeaveMessage(txn, s, false); + Message sent = sendLeaveMessage(txn, s); // Move to the DISSOLVED state return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(), sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), @@ -276,6 +276,6 @@ class CreatorProtocolEngine extends AbstractProtocolEngine { SessionId sessionId = new SessionId(m.getPrivateGroupId().getBytes()); return new GroupInvitationResponse(m.getId(), m.getContactGroupId(), m.getTimestamp(), false, false, false, false, sessionId, - accept, m.getPrivateGroupId(), m.getAutoDeleteTimer()); + accept, m.getPrivateGroupId(), m.getAutoDeleteTimer(), false); } } diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java index 82efba05f..ede12734a 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java @@ -11,6 +11,7 @@ interface GroupInvitationConstants { String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer"; String MSG_KEY_INVITATION_ACCEPTED = "invitationAccepted"; String MSG_KEY_AUTO_DELETE_TIMER = "autoDeleteTimer"; + String MSG_KEY_IS_AUTO_DECLINE = "isAutoDecline"; // Session keys String SESSION_KEY_IS_SESSION = "isSession"; diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java index c34a23c3e..ece8ed827 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java @@ -1,6 +1,7 @@ package org.briarproject.briar.privategroup.invitation; import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.cleanup.CleanupHook; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.client.ContactGroupFactory; import org.briarproject.bramble.api.contact.Contact; @@ -24,6 +25,7 @@ import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageStatus; import org.briarproject.bramble.api.versioning.ClientVersioningManager; import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook; +import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent; import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.conversation.ConversationMessageHeader; @@ -52,6 +54,7 @@ import javax.annotation.concurrent.Immutable; import javax.inject.Inject; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; +import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; import static org.briarproject.briar.privategroup.invitation.CreatorState.START; import static org.briarproject.briar.privategroup.invitation.MessageType.ABORT; import static org.briarproject.briar.privategroup.invitation.MessageType.INVITE; @@ -65,7 +68,7 @@ import static org.briarproject.briar.privategroup.invitation.Role.PEER; @NotNullByDefault class GroupInvitationManagerImpl extends ConversationClientImpl implements GroupInvitationManager, OpenDatabaseHook, ContactHook, - PrivateGroupHook, ClientVersioningHook { + PrivateGroupHook, ClientVersioningHook, CleanupHook { private final ClientVersioningManager clientVersioningManager; private final ContactGroupFactory contactGroupFactory; @@ -148,6 +151,11 @@ class GroupInvitationManagerImpl extends ConversationClientImpl BdfDictionary bdfMeta) throws DbException, FormatException { // Parse the metadata MessageMetadata meta = messageParser.parseMetadata(bdfMeta); + // set the clean-up timer that will be started when message gets read + long timer = meta.getAutoDeleteTimer(); + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } // Look up the session, if there is one SessionId sessionId = getSessionId(meta.getPrivateGroupId()); StoredSession ss = getSession(txn, m.getGroupId(), sessionId); @@ -301,7 +309,13 @@ class GroupInvitationManagerImpl extends ConversationClientImpl @Override public void respondToInvitation(ContactId c, SessionId sessionId, boolean accept) throws DbException { - Transaction txn = db.startTransaction(false); + db.transaction(false, + txn -> respondToInvitation(txn, c, sessionId, accept, false)); + } + + private void respondToInvitation(Transaction txn, ContactId c, + SessionId sessionId, boolean accept, boolean isAutoDecline) + throws DbException { try { // Look up the session Contact contact = db.getContact(txn, c); @@ -313,14 +327,12 @@ class GroupInvitationManagerImpl extends ConversationClientImpl .parseInviteeSession(contactGroupId, ss.bdfSession); // Handle the join or leave action if (accept) session = inviteeEngine.onJoinAction(txn, session); - else session = inviteeEngine.onLeaveAction(txn, session); + else session = + inviteeEngine.onLeaveAction(txn, session, isAutoDecline); // Store the updated session storeSession(txn, ss.storageId, session); - db.commitTransaction(txn); } catch (FormatException e) { throw new DbException(e); - } finally { - db.endTransaction(txn); } } @@ -356,7 +368,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl } else if (type == LocalAction.JOIN) { return engine.onJoinAction(txn, session); } else if (type == LocalAction.LEAVE) { - return engine.onLeaveAction(txn, session); + return engine.onLeaveAction(txn, session, false); } else if (type == LocalAction.MEMBER_ADDED) { return engine.onMemberAddedAction(txn, session); } else { @@ -425,7 +437,8 @@ class GroupInvitationManagerImpl extends ConversationClientImpl return new GroupInvitationResponse(m, contactGroupId, meta.getTimestamp(), meta.isLocal(), meta.isRead(), status.isSent(), status.isSeen(), sessionId, accept, - meta.getPrivateGroupId(), meta.getAutoDeleteTimer()); + meta.getPrivateGroupId(), meta.getAutoDeleteTimer(), + meta.isAutoDecline()); } @Override @@ -686,7 +699,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl // get ID of the contact group GroupId g = getContactGroup(db.getContact(txn, c)).getId(); - // get metadata for all messages in the + // get metadata for all messages in the group // (these are sessions *and* protocol messages) Map metadata; try { @@ -762,6 +775,57 @@ class GroupInvitationManagerImpl extends ConversationClientImpl return result; } + @Override + public void deleteMessages(Transaction txn, GroupId g, + Collection messageIds) throws DbException { + ContactId c; + Map sessions = new HashMap<>(); + try { + // get the ContactId from the given GroupId + c = clientHelper.getContactId(txn, g); + // get sessions for all messages to be deleted + for (MessageId messageId : messageIds) { + BdfDictionary d = clientHelper + .getMessageMetadataAsDictionary(txn, messageId); + MessageMetadata messageMetadata = + messageParser.parseMetadata(d); + if (!messageMetadata.isVisibleInConversation()) + throw new IllegalArgumentException(); + SessionId sessionId = + getSessionId(messageMetadata.getPrivateGroupId()); + DeletableSession deletableSession = sessions.get(sessionId); + if (deletableSession == null) { + StoredSession ss = getSession(txn, g, sessionId); + if (ss == null) throw new DbException(); + Session session = + sessionParser.parseSession(g, ss.bdfSession); + deletableSession = new DeletableSession(session.getState()); + sessions.put(sessionId, deletableSession); + } + deletableSession.messages.add(messageId); + } + } catch (FormatException e) { + throw new DbException(e); + } + + // delete given visible messages in sessions and auto-respond before + for (Entry entry : sessions.entrySet()) { + DeletableSession session = entry.getValue(); + // decline invitee sessions waiting for a response before + if (session.state instanceof InviteeState && + session.state.isAwaitingResponse()) { + respondToInvitation(txn, c, entry.getKey(), false, true); + } + for (MessageId m : session.messages) { + db.deleteMessage(txn, m); + db.deleteMessageMetadata(txn, m); + } + } + recalculateGroupCount(txn, g); + + txn.attach(new ConversationMessagesDeletedEvent(c, messageIds)); + } + @Override public Set getMessageIds(Transaction txn, ContactId c) throws DbException { diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java index 0628357e3..fa0b09019 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java @@ -1,5 +1,6 @@ package org.briarproject.briar.privategroup.invitation; +import org.briarproject.bramble.api.cleanup.CleanupManager; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.data.MetadataEncoder; @@ -41,7 +42,8 @@ public class GroupInvitationModule { ValidationManager validationManager, ContactManager contactManager, PrivateGroupManager privateGroupManager, ConversationManager conversationManager, - ClientVersioningManager clientVersioningManager) { + ClientVersioningManager clientVersioningManager, + CleanupManager cleanupManager) { lifecycleManager.registerOpenDatabaseHook(groupInvitationManager); validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION, groupInvitationManager); @@ -56,6 +58,8 @@ public class GroupInvitationModule { PrivateGroupManager.MAJOR_VERSION, PrivateGroupManager.MINOR_VERSION, groupInvitationManager.getPrivateGroupClientVersioningHook()); + cleanupManager.registerCleanupHook(CLIENT_ID, MAJOR_VERSION, + groupInvitationManager); return groupInvitationManager; } diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidator.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidator.java index 9b9661ea9..34111cf04 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidator.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidator.java @@ -110,8 +110,7 @@ class GroupInvitationValidator extends BdfMessageValidator { } // Create the metadata BdfDictionary meta = messageEncoder.encodeMetadata(INVITE, - privateGroup.getId(), m.getTimestamp(), false, false, false, - false, false, timer); + privateGroup.getId(), m.getTimestamp(), timer); return new BdfMessageContext(meta); } @@ -132,8 +131,7 @@ class GroupInvitationValidator extends BdfMessageValidator { } BdfDictionary meta = messageEncoder.encodeMetadata(JOIN, - new GroupId(privateGroupId), m.getTimestamp(), false, false, - false, false, false, timer); + new GroupId(privateGroupId), m.getTimestamp(), timer); if (previousMessageId == null) { return new BdfMessageContext(meta); } else { @@ -160,8 +158,7 @@ class GroupInvitationValidator extends BdfMessageValidator { } BdfDictionary meta = messageEncoder.encodeMetadata(LEAVE, - new GroupId(privateGroupId), m.getTimestamp(), false, false, - false, false, false, timer); + new GroupId(privateGroupId), m.getTimestamp(), timer); if (previousMessageId == null) { return new BdfMessageContext(meta); } else { @@ -177,8 +174,8 @@ class GroupInvitationValidator extends BdfMessageValidator { byte[] privateGroupId = body.getRaw(1); checkLength(privateGroupId, UniqueId.LENGTH); BdfDictionary meta = messageEncoder.encodeMetadata(ABORT, - new GroupId(privateGroupId), m.getTimestamp(), false, false, - false, false, false, NO_AUTO_DELETE_TIMER); + new GroupId(privateGroupId), m.getTimestamp(), + NO_AUTO_DELETE_TIMER); return new BdfMessageContext(meta); } } diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java index d7fdd070b..5afe1c443 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java @@ -89,8 +89,8 @@ class InviteeProtocolEngine extends AbstractProtocolEngine { } @Override - public InviteeSession onLeaveAction(Transaction txn, InviteeSession s) - throws DbException { + public InviteeSession onLeaveAction(Transaction txn, InviteeSession s, + boolean isAutoDecline) throws DbException { switch (s.getState()) { case START: case LEFT: @@ -98,7 +98,7 @@ class InviteeProtocolEngine extends AbstractProtocolEngine { case ERROR: return s; // Ignored in these states case INVITED: - return onLocalDecline(txn, s); + return onLocalDecline(txn, s, isAutoDecline); case ACCEPTED: case JOINED: return onLocalLeave(txn, s); @@ -203,14 +203,14 @@ class InviteeProtocolEngine extends AbstractProtocolEngine { s.getInviteTimestamp(), ACCEPTED); } - private InviteeSession onLocalDecline(Transaction txn, InviteeSession s) - throws DbException { + private InviteeSession onLocalDecline(Transaction txn, InviteeSession s, + boolean isAutoDecline) throws DbException { // Mark the invite message unavailable to answer MessageId inviteId = s.getLastRemoteMessageId(); if (inviteId == null) throw new IllegalStateException(); markMessageAvailableToAnswer(txn, inviteId, false); // Send a LEAVE message - Message sent = sendLeaveMessage(txn, s, true); + Message sent = sendLeaveMessage(txn, s, true, isAutoDecline); // Track the message messageTracker.trackOutgoingMessage(txn, sent); // Move to the START state @@ -222,7 +222,7 @@ class InviteeProtocolEngine extends AbstractProtocolEngine { private InviteeSession onLocalLeave(Transaction txn, InviteeSession s) throws DbException { // Send a LEAVE message - Message sent = sendLeaveMessage(txn, s, false); + Message sent = sendLeaveMessage(txn, s); try { // Make the private group invisible to the contact setPrivateGroupVisibility(txn, s, INVISIBLE); diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoder.java index 7f65a8c42..72e8b91d6 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoder.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoder.java @@ -14,7 +14,11 @@ interface MessageEncoder { BdfDictionary encodeMetadata(MessageType type, GroupId privateGroupId, long timestamp, boolean local, boolean read, boolean visible, - boolean available, boolean accepted, long autoDeleteTimer); + boolean available, boolean accepted, long autoDeleteTimer, + boolean isAutoDecline); + + BdfDictionary encodeMetadata(MessageType type, GroupId privateGroupId, + long timestamp, long autoDeleteTimer); void setVisibleInUi(BdfDictionary meta, boolean visible); diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoderImpl.java index 8027ebf07..2ef288951 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoderImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoderImpl.java @@ -20,6 +20,7 @@ import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AUTO_DELETE_TIMER; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AVAILABLE_TO_ANSWER; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_INVITATION_ACCEPTED; +import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_IS_AUTO_DECLINE; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_LOCAL; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_MESSAGE_TYPE; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_PRIVATE_GROUP_ID; @@ -48,7 +49,7 @@ class MessageEncoderImpl implements MessageEncoder { public BdfDictionary encodeMetadata(MessageType type, GroupId privateGroupId, long timestamp, boolean local, boolean read, boolean visible, boolean available, boolean accepted, - long autoDeleteTimer) { + long autoDeleteTimer, boolean isAutoDecline) { BdfDictionary meta = new BdfDictionary(); meta.put(MSG_KEY_MESSAGE_TYPE, type.getValue()); meta.put(MSG_KEY_PRIVATE_GROUP_ID, privateGroupId); @@ -61,9 +62,19 @@ class MessageEncoderImpl implements MessageEncoder { if (autoDeleteTimer != NO_AUTO_DELETE_TIMER) { meta.put(MSG_KEY_AUTO_DELETE_TIMER, autoDeleteTimer); } + if (isAutoDecline) { + meta.put(MSG_KEY_IS_AUTO_DECLINE, isAutoDecline); + } return meta; } + @Override + public BdfDictionary encodeMetadata(MessageType type, + GroupId privateGroupId, long timestamp, long autoDeleteTimer) { + return encodeMetadata(type, privateGroupId, timestamp, false, false, + false, false, false, autoDeleteTimer, false); + } + @Override public void setVisibleInUi(BdfDictionary meta, boolean visible) { meta.put(MSG_KEY_VISIBLE_IN_UI, visible); diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageMetadata.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageMetadata.java index f847a98fd..51812f86d 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageMetadata.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageMetadata.java @@ -13,10 +13,12 @@ class MessageMetadata { private final GroupId privateGroupId; private final long timestamp, autoDeleteTimer; private final boolean local, read, visible, available, accepted; + private final boolean isAutoDecline; MessageMetadata(MessageType type, GroupId privateGroupId, long timestamp, boolean local, boolean read, boolean visible, - boolean available, boolean accepted, long autoDeleteTimer) { + boolean available, boolean accepted, long autoDeleteTimer, + boolean isAutoDecline) { this.privateGroupId = privateGroupId; this.type = type; this.timestamp = timestamp; @@ -26,6 +28,7 @@ class MessageMetadata { this.available = available; this.accepted = accepted; this.autoDeleteTimer = autoDeleteTimer; + this.isAutoDecline = isAutoDecline; } MessageType getMessageType() { @@ -58,7 +61,7 @@ class MessageMetadata { /** * Returns true if the invitation was accepted. - * + *

* Only applies to messages of type INVITE. */ public boolean wasAccepted() { @@ -68,4 +71,8 @@ class MessageMetadata { public long getAutoDeleteTimer() { return autoDeleteTimer; } + + public boolean isAutoDecline() { + return isAutoDecline; + } } diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java index 0fdc19c8b..aac5ec988 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java @@ -23,6 +23,7 @@ import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AUTO_DELETE_TIMER; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AVAILABLE_TO_ANSWER; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_INVITATION_ACCEPTED; +import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_IS_AUTO_DECLINE; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_LOCAL; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_MESSAGE_TYPE; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_PRIVATE_GROUP_ID; @@ -82,8 +83,9 @@ class MessageParserImpl implements MessageParser { boolean accepted = meta.getBoolean(MSG_KEY_INVITATION_ACCEPTED, false); long timer = meta.getLong(MSG_KEY_AUTO_DELETE_TIMER, NO_AUTO_DELETE_TIMER); + boolean isAutoDecline = meta.getBoolean(MSG_KEY_IS_AUTO_DECLINE, false); return new MessageMetadata(type, privateGroupId, timestamp, local, read, - visible, available, accepted, timer); + visible, available, accepted, timer, isAutoDecline); } @Override diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java index a8c00fd25..626cb6093 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java @@ -85,8 +85,8 @@ class PeerProtocolEngine extends AbstractProtocolEngine { } @Override - public PeerSession onLeaveAction(Transaction txn, PeerSession s) - throws DbException { + public PeerSession onLeaveAction(Transaction txn, PeerSession s, + boolean isAutoDecline) throws DbException { switch (s.getState()) { case START: case AWAIT_MEMBER: @@ -213,7 +213,7 @@ class PeerProtocolEngine extends AbstractProtocolEngine { private PeerSession onLocalLeaveFromBothJoined(Transaction txn, PeerSession s) throws DbException { // Send a LEAVE message - Message sent = sendLeaveMessage(txn, s, false); + Message sent = sendLeaveMessage(txn, s); try { // Make the private group invisible to the contact setPrivateGroupVisibility(txn, s, INVISIBLE); @@ -229,7 +229,7 @@ class PeerProtocolEngine extends AbstractProtocolEngine { private PeerSession onLocalLeaveFromLocalJoined(Transaction txn, PeerSession s) throws DbException { // Send a LEAVE message - Message sent = sendLeaveMessage(txn, s, false); + Message sent = sendLeaveMessage(txn, s); try { // Make the private group invisible to the contact setPrivateGroupVisibility(txn, s, INVISIBLE); diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngine.java index ec4010ff7..d44359d89 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngine.java @@ -16,7 +16,14 @@ interface ProtocolEngine> { S onJoinAction(Transaction txn, S session) throws DbException; - S onLeaveAction(Transaction txn, S session) throws DbException; + /** + * Leaves the group or declines an invitation. + * + * @param isAutoDecline true if automatically declined due to deletion + * and false if initiated by the user. + */ + S onLeaveAction(Transaction txn, S session, boolean isAutoDecline) + throws DbException; S onMemberAddedAction(Transaction txn, S session) throws DbException; diff --git a/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java b/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java new file mode 100644 index 000000000..74acfd83b --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java @@ -0,0 +1,180 @@ +package org.briarproject.briar.autodelete; + +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventListener; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.system.TimeTravelModule; +import org.briarproject.bramble.test.TestDatabaseConfigModule; +import org.briarproject.briar.api.autodelete.AutoDeleteManager; +import org.briarproject.briar.api.client.MessageTracker.GroupCount; +import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient; +import org.briarproject.briar.api.conversation.ConversationMessageHeader; +import org.briarproject.briar.api.messaging.MessagingManager; +import org.briarproject.briar.test.BriarIntegrationTest; +import org.briarproject.briar.test.BriarIntegrationTestComponent; +import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent; +import org.junit.Before; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import javax.annotation.Nonnull; + +import static java.util.Collections.sort; +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public abstract class AbstractAutoDeleteTest extends + BriarIntegrationTest { + + protected final long startTime = System.currentTimeMillis(); + + protected abstract ConversationClient getConversationClient( + BriarIntegrationTestComponent component); + + @Override + protected void createComponents() { + BriarIntegrationTestComponent component = + DaggerBriarIntegrationTestComponent.builder().build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(component); + component.inject(this); + + c0 = DaggerBriarIntegrationTestComponent.builder() + .testDatabaseConfigModule(new TestDatabaseConfigModule(t0Dir)) + .timeTravelModule(new TimeTravelModule(true)) + .build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(c0); + + c1 = DaggerBriarIntegrationTestComponent.builder() + .testDatabaseConfigModule(new TestDatabaseConfigModule(t1Dir)) + .timeTravelModule(new TimeTravelModule(true)) + .build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(c1); + + c2 = DaggerBriarIntegrationTestComponent.builder() + .testDatabaseConfigModule(new TestDatabaseConfigModule(t2Dir)) + .timeTravelModule(new TimeTravelModule(true)) + .build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(c2); + + // Use different times to avoid creating identical messages that are + // treated as redundant copies of the same message (#1907) + try { + c0.getTimeTravel().setCurrentTimeMillis(startTime); + c1.getTimeTravel().setCurrentTimeMillis(startTime + 1); + c2.getTimeTravel().setCurrentTimeMillis(startTime + 2); + } catch (InterruptedException e) { + fail(); + } + } + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + // Run the initial cleanup task that was scheduled at startup + c0.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); + c1.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); + c2.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); + } + + protected List getMessageHeaders( + BriarIntegrationTestComponent component, ContactId contactId) + throws Exception { + DatabaseComponent db = component.getDatabaseComponent(); + ConversationClient conversationClient = + getConversationClient(component); + return sortHeaders(db.transactionWithResult(true, txn -> + conversationClient.getMessageHeaders(txn, contactId))); + } + + @SuppressWarnings({"UseCompareMethod", "Java8ListSort"}) // Animal Sniffer + protected List sortHeaders( + Collection in) { + List out = new ArrayList<>(in); + sort(out, (a, b) -> + Long.valueOf(a.getTimestamp()).compareTo(b.getTimestamp())); + return out; + } + + @FunctionalInterface + protected interface HeaderConsumer { + void accept(ConversationMessageHeader header); + } + + protected void forEachHeader(BriarIntegrationTestComponent component, + ContactId contactId, int size, HeaderConsumer consumer) + throws Exception { + List headers = + getMessageHeaders(component, contactId); + assertEquals(size, headers.size()); + for (ConversationMessageHeader h : headers) consumer.accept(h); + } + + protected long getAutoDeleteTimer(BriarIntegrationTestComponent component, + ContactId contactId) throws DbException { + DatabaseComponent db = component.getDatabaseComponent(); + AutoDeleteManager autoDeleteManager = component.getAutoDeleteManager(); + return db.transactionWithResult(false, + txn -> autoDeleteManager.getAutoDeleteTimer(txn, contactId)); + } + + protected void markMessageRead(BriarIntegrationTestComponent component, + Contact contact, MessageId messageId) throws Exception { + MessagingManager messagingManager = component.getMessagingManager(); + ConversationClient conversationClient = + getConversationClient(component); + GroupId groupId = conversationClient.getContactGroup(contact).getId(); + messagingManager.setReadFlag(groupId, messageId, true); + waitForEvents(component); + } + + protected void assertGroupCount(BriarIntegrationTestComponent component, + ContactId contactId, int messageCount, int unreadCount) + throws DbException { + DatabaseComponent db = component.getDatabaseComponent(); + ConversationClient conversationClient = + getConversationClient(component); + + GroupCount gc = db.transactionWithResult(true, txn -> + conversationClient.getGroupCount(txn, contactId)); + assertEquals("messageCount", messageCount, gc.getMsgCount()); + assertEquals("unreadCount", unreadCount, gc.getUnreadCount()); + } + + /** + * Broadcasts a marker event and waits for it to be delivered, which + * indicates that all previously broadcast events have been delivered. + */ + protected void waitForEvents(BriarIntegrationTestComponent component) + throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MarkerEvent marker = new MarkerEvent(); + EventBus eventBus = component.getEventBus(); + eventBus.addListener(new EventListener() { + @Override + public void eventOccurred(@Nonnull Event e) { + if (e == marker) { + latch.countDown(); + eventBus.removeListener(this); + } + } + }); + eventBus.broadcast(marker); + if (!latch.await(1, MINUTES)) fail(); + } + + private static class MarkerEvent extends Event { + } + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java index da5363e12..911eedcf8 100644 --- a/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java @@ -1,44 +1,29 @@ package org.briarproject.briar.messaging; -import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.MessageDeletedException; -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.event.EventBus; -import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.system.TimeTravelModule; -import org.briarproject.bramble.test.TestDatabaseConfigModule; import org.briarproject.briar.api.attachment.AttachmentHeader; import org.briarproject.briar.api.autodelete.AutoDeleteManager; -import org.briarproject.briar.api.client.MessageTracker.GroupCount; import org.briarproject.briar.api.conversation.ConversationManager; +import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient; import org.briarproject.briar.api.conversation.ConversationMessageHeader; import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessageFactory; -import org.briarproject.briar.test.BriarIntegrationTest; +import org.briarproject.briar.autodelete.AbstractAutoDeleteTest; import org.briarproject.briar.test.BriarIntegrationTestComponent; -import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent; -import org.junit.Before; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.concurrent.CountDownLatch; - -import javax.annotation.Nonnull; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; -import static java.util.Collections.sort; -import static java.util.concurrent.TimeUnit.MINUTES; import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS; import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS; @@ -47,56 +32,13 @@ import static org.briarproject.briar.messaging.MessagingConstants.MISSING_ATTACH import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -public class AutoDeleteIntegrationTest - extends BriarIntegrationTest { +public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { @Override - protected void createComponents() { - BriarIntegrationTestComponent component = - DaggerBriarIntegrationTestComponent.builder().build(); - BriarIntegrationTestComponent.Helper.injectEagerSingletons(component); - component.inject(this); - - c0 = DaggerBriarIntegrationTestComponent.builder() - .testDatabaseConfigModule(new TestDatabaseConfigModule(t0Dir)) - .timeTravelModule(new TimeTravelModule(true)) - .build(); - BriarIntegrationTestComponent.Helper.injectEagerSingletons(c0); - - c1 = DaggerBriarIntegrationTestComponent.builder() - .testDatabaseConfigModule(new TestDatabaseConfigModule(t1Dir)) - .timeTravelModule(new TimeTravelModule(true)) - .build(); - BriarIntegrationTestComponent.Helper.injectEagerSingletons(c1); - - c2 = DaggerBriarIntegrationTestComponent.builder() - .testDatabaseConfigModule(new TestDatabaseConfigModule(t2Dir)) - .timeTravelModule(new TimeTravelModule(true)) - .build(); - BriarIntegrationTestComponent.Helper.injectEagerSingletons(c2); - - // Use different times to avoid creating identical messages that are - // treated as redundant copies of the same message (#1907) - try { - long now = System.currentTimeMillis(); - c0.getTimeTravel().setCurrentTimeMillis(now); - c1.getTimeTravel().setCurrentTimeMillis(now + 1); - c2.getTimeTravel().setCurrentTimeMillis(now + 2); - } catch (InterruptedException e) { - fail(); - } - } - - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - // Run the initial cleanup task that was scheduled at startup - c0.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); - c1.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); - c2.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); + protected ConversationClient getConversationClient( + BriarIntegrationTestComponent component) { + return component.getMessagingManager(); } @Test @@ -216,7 +158,6 @@ public class AutoDeleteIntegrationTest assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -293,7 +234,6 @@ public class AutoDeleteIntegrationTest assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId0); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -355,7 +295,6 @@ public class AutoDeleteIntegrationTest assertEquals(0, getMessageHeaders(c1, contactId0From1).size()); // 0 marks the message as read - this starts 0's timer markMessageRead(c0, contact1From0, messageId1); - waitForEvents(c0); assertGroupCount(c0, contactId1From0, 1, 0); // Before 0's timer elapses, 0 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -426,7 +365,6 @@ public class AutoDeleteIntegrationTest assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId())); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -501,7 +439,6 @@ public class AutoDeleteIntegrationTest assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -603,7 +540,6 @@ public class AutoDeleteIntegrationTest assertTrue(messageIsDeleted(c1, attachmentHeader.getMessageId())); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -711,7 +647,6 @@ public class AutoDeleteIntegrationTest assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId())); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -808,33 +743,6 @@ public class AutoDeleteIntegrationTest db.transaction(false, txn -> db.setMessageShared(txn, messageId)); } - private List getMessageHeaders( - BriarIntegrationTestComponent component, ContactId contactId) - throws Exception { - DatabaseComponent db = component.getDatabaseComponent(); - MessagingManager messagingManager = component.getMessagingManager(); - - return sortHeaders(db.transactionWithResult(true, txn -> - messagingManager.getMessageHeaders(txn, contactId))); - } - - private long getAutoDeleteTimer(BriarIntegrationTestComponent component, - ContactId contactId) throws DbException { - DatabaseComponent db = component.getDatabaseComponent(); - AutoDeleteManager autoDeleteManager = component.getAutoDeleteManager(); - - return db.transactionWithResult(false, - txn -> autoDeleteManager.getAutoDeleteTimer(txn, contactId)); - } - - private void markMessageRead(BriarIntegrationTestComponent component, - Contact contact, MessageId messageId) throws DbException { - MessagingManager messagingManager = component.getMessagingManager(); - - GroupId groupId = messagingManager.getContactGroup(contact).getId(); - messagingManager.setReadFlag(groupId, messageId, true); - } - private boolean messageIsDeleted(BriarIntegrationTestComponent component, MessageId messageId) throws DbException { DatabaseComponent db = component.getDatabaseComponent(); @@ -847,49 +755,4 @@ public class AutoDeleteIntegrationTest } } - private void assertGroupCount(BriarIntegrationTestComponent component, - ContactId contactId, int messageCount, int unreadCount) - throws DbException { - DatabaseComponent db = component.getDatabaseComponent(); - MessagingManager messagingManager = component.getMessagingManager(); - - GroupCount gc = db.transactionWithResult(true, txn -> - messagingManager.getGroupCount(txn, contactId)); - assertEquals(messageCount, gc.getMsgCount()); - assertEquals(unreadCount, gc.getUnreadCount()); - } - - @SuppressWarnings({"UseCompareMethod", "Java8ListSort"}) // Animal Sniffer - private List sortHeaders( - Collection in) { - List out = new ArrayList<>(in); - sort(out, (a, b) -> - Long.valueOf(a.getTimestamp()).compareTo(b.getTimestamp())); - return out; - } - - /** - * Broadcasts a marker event and waits for it to be delivered, which - * indicates that all previously broadcast events have been delivered. - */ - private void waitForEvents(BriarIntegrationTestComponent component) - throws Exception { - CountDownLatch latch = new CountDownLatch(1); - MarkerEvent marker = new MarkerEvent(); - EventBus eventBus = component.getEventBus(); - eventBus.addListener(new EventListener() { - @Override - public void eventOccurred(@Nonnull Event e) { - if (e == marker) { - latch.countDown(); - eventBus.removeListener(this); - } - } - }); - eventBus.broadcast(marker); - if (!latch.await(1, MINUTES)) fail(); - } - - private static class MarkerEvent extends Event { - } } diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java index 1978d2b7f..b106ec10b 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java @@ -194,7 +194,7 @@ abstract class AbstractProtocolEngineTest extends BrambleMockTestCase { context.checking(new Expectations() {{ oneOf(messageEncoder).encodeMetadata(type, privateGroupId, message.getTimestamp(), true, true, visible, false, false, - NO_AUTO_DELETE_TIMER); + NO_AUTO_DELETE_TIMER, false); will(returnValue(meta)); oneOf(clientHelper).addLocalMessage(txn, message, meta, true, false); diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AutoDeleteIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AutoDeleteIntegrationTest.java new file mode 100644 index 000000000..b625f3192 --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AutoDeleteIntegrationTest.java @@ -0,0 +1,456 @@ +package org.briarproject.briar.privategroup.invitation; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient; +import org.briarproject.briar.api.privategroup.GroupMessage; +import org.briarproject.briar.api.privategroup.PrivateGroup; +import org.briarproject.briar.api.privategroup.PrivateGroupManager; +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest; +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse; +import org.briarproject.briar.autodelete.AbstractAutoDeleteTest; +import org.briarproject.briar.test.BriarIntegrationTestComponent; +import org.junit.Before; +import org.junit.Test; + +import javax.annotation.Nullable; + +import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS; +import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS; +import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { + + private PrivateGroup privateGroup; + private PrivateGroupManager groupManager0; + private GroupInvitationManager groupInvitationManager0, + groupInvitationManager1; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + groupManager0 = c0.getPrivateGroupManager(); + groupInvitationManager0 = c0.getGroupInvitationManager(); + groupInvitationManager1 = c1.getGroupInvitationManager(); + privateGroup = addPrivateGroup("Testgroup", startTime); + } + + @Override + protected ConversationClient getConversationClient( + BriarIntegrationTestComponent component) { + return component.getGroupInvitationManager(); + } + + @Test + public void testInvitationAutoDecline() throws Exception { + setAutoDeleteTimer(c0, contact1From0.getId(), MIN_AUTO_DELETE_TIMER_MS); + + // Send invitation + sendInvitation(privateGroup, contact1From0.getId(), "Join this!"); + + // The message should have been added to 0's view of the conversation + assertGroupCount(c0, contactId1From0, 1, 0); + forEachHeader(c0, contactId1From0, 1, h -> { + // The message should have the expected timer + assertEquals(MIN_AUTO_DELETE_TIMER_MS, h.getAutoDeleteTimer()); + }); + + // Sync the message to 1 + sync0To1(1, true); + // Sync the ack to 0 - this starts 0's timer + ack1To0(1); + waitForEvents(c0); + + // The message should have been added to 1's view of the conversation + assertGroupCount(c1, contactId0From1, 1, 1); + forEachHeader(c1, contactId0From1, 1, h -> { + // The message should have the expected timer + assertEquals(MIN_AUTO_DELETE_TIMER_MS, h.getAutoDeleteTimer()); + }); + + // Both peers should be using the new timer + assertEquals(MIN_AUTO_DELETE_TIMER_MS, + getAutoDeleteTimer(c0, contactId1From0)); + assertEquals(MIN_AUTO_DELETE_TIMER_MS, + getAutoDeleteTimer(c1, contactId0From1)); + + // Before 0's timer elapses, both peers should still see the message + long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS; + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 1); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + + // When 0's timer has elapsed, the message should be deleted from 0's + // view of the conversation but 1 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 1); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + + // 1 marks the message as read - this starts 1's timer + final MessageId messageId0 = + getMessageHeaders(c1, contactId0From1).get(0).getId(); + markMessageRead(c1, contact0From1, messageId0); + assertGroupCount(c1, contactId0From1, 1, 0); + + // Before 1's timer elapses, 1 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 0); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + + // When 1's timer has elapsed, the message should be deleted from 1's + // view of the conversation and the invitation auto-declined + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 0); + forEachHeader(c1, contactId0From1, 1, h -> { + // The only message is not the same as before, but declined response + assertNotEquals(messageId0, h.getId()); + assertTrue(h instanceof GroupInvitationResponse); + assertFalse(((GroupInvitationResponse) h).wasAccepted()); + assertTrue(((GroupInvitationResponse) h).isAutoDecline()); + // The auto-decline message should have the expected timer + assertEquals(MIN_AUTO_DELETE_TIMER_MS, + h.getAutoDeleteTimer()); + }); + + // Sync the auto-decline message to 0 + sync1To0(1, true); + // Sync the ack to 1 - this starts 1's timer + ack0To1(1); + waitForEvents(c1); + + // 0 can invite 1 again + assertTrue(groupInvitationManager0 + .isInvitationAllowed(contact1From0, privateGroup.getId())); + + // Before 1's timer elapses, 1 should still see the auto-decline message + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 1, 1); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 0); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + // When 1's timer has elapsed, the auto-decline message should be + // deleted from 1's view of the conversation + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 1, 1); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 0, 0); + assertEquals(0, getMessageHeaders(c1, contactId0From1).size()); + + // 0 marks the message as read - this starts 0's timer + MessageId messageId1 = + getMessageHeaders(c0, contactId1From0).get(0).getId(); + markMessageRead(c0, contact1From0, messageId1); + assertGroupCount(c0, contactId1From0, 1, 0); + + // Before 0's timer elapses, 0 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + + // When 0's timer has elapsed, the message should be deleted from 0's + // view of the conversation + c0.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + + // 0 can invite 1 again and really does invite + assertTrue(groupInvitationManager0 + .isInvitationAllowed(contact1From0, privateGroup.getId())); + sendInvitation(privateGroup, contact1From0.getId(), + "Join this faster please!"); + sync0To1(1, true); + assertGroupCount(c1, contactId0From1, 1, 1); + } + + @Test + public void testAutoDeleteDoesNotRemoveOtherSessions() throws Exception { + PrivateGroup pg = addPrivateGroup("Another one", startTime + 1); + + // Send invitation for another group without timer + sendInvitation(pg, contact1From0.getId(), null); + sync0To1(1, true); + ack1To0(1); + waitForEvents(c0); + + // The message should have been added the views of the conversation + assertGroupCount(c0, contactId1From0, 1, 0); + assertGroupCount(c1, contactId0From1, 1, 1); + // The message should have no timer for either peer + forEachHeader(c0, contactId1From0, 1, h -> + assertEquals(NO_AUTO_DELETE_TIMER, h.getAutoDeleteTimer())); + forEachHeader(c1, contactId0From1, 1, h -> + assertEquals(NO_AUTO_DELETE_TIMER, h.getAutoDeleteTimer())); + + // enable timer + setAutoDeleteTimer(c0, contact1From0.getId(), MIN_AUTO_DELETE_TIMER_MS); + + // Send invitation, ACK it and check group counts + sendInvitation(privateGroup, contact1From0.getId(), "Join this!"); + sync0To1(1, true); + ack1To0(1); + waitForEvents(c0); + assertGroupCount(c0, contactId1From0, 2, 0); + assertGroupCount(c1, contactId0From1, 2, 2); + + // Both peers should be using the new timer + assertEquals(MIN_AUTO_DELETE_TIMER_MS, + getAutoDeleteTimer(c0, contactId1From0)); + assertEquals(MIN_AUTO_DELETE_TIMER_MS, + getAutoDeleteTimer(c1, contactId0From1)); + + // Before 0's timer elapses, both peers should still see the message + long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS; + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 2, 0); + assertEquals(2, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 2, 2); + assertEquals(2, getMessageHeaders(c1, contactId0From1).size()); + + // When 0's timer has elapsed, the message should be deleted from 0's + // view of the conversation but 1 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 2, 2); + assertEquals(2, getMessageHeaders(c1, contactId0From1).size()); + + // 1 marks all the message as read - this starts 1's timer for 2nd msg + forEachHeader(c1, contactId0From1, 2, h -> { + try { + markMessageRead(c1, contact0From1, h.getId()); + } catch (Exception e) { + fail(); + } + }); + assertGroupCount(c1, contactId0From1, 2, 0); + + // Before 1's timer elapses, 1 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 2, 0); + assertEquals(2, getMessageHeaders(c1, contactId0From1).size()); + + // When 1's timer has elapsed, the message should be deleted from 1's + // view of the conversation and the invitation auto-declined + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + // 1's total count is still 2, because of the added auto-decline + assertGroupCount(c1, contactId0From1, 2, 0); + forEachHeader(c1, contactId0From1, 2, h -> { + if (h instanceof GroupInvitationRequest) { + // the request is for the first invitation + assertEquals(pg.getId(), + ((GroupInvitationRequest) h).getNameable().getId()); + } else { + assertTrue(h instanceof GroupInvitationResponse); + GroupInvitationResponse r = (GroupInvitationResponse) h; + // is auto-decline for 2nd invitation + assertEquals(privateGroup.getId(), r.getShareableId()); + assertTrue(r.isAutoDecline()); + assertFalse(r.wasAccepted()); + } + }); + + // Sync the auto-decline message to 0 + sync1To0(1, true); + // Sync the ack to 1 - this starts 1's timer + ack0To1(1); + waitForEvents(c1); + // 0 marks the message as read - this starts 0's timer + GroupInvitationResponse autoDeclineMessage = (GroupInvitationResponse) + getMessageHeaders(c0, contactId1From0).get(1); + markMessageRead(c0, contact1From0, autoDeclineMessage.getId()); + assertGroupCount(c0, contactId1From0, 2, 0); + assertGroupCount(c1, contactId0From1, 2, 0); + + // Timer of auto-decline elapses for both peers at the same time + c0.getTimeTravel().addCurrentTimeMillis(timerLatency); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency); + assertGroupCount(c0, contactId1From0, 1, 0); + assertGroupCount(c1, contactId0From1, 1, 0); + + // 1 responds to first invitation (that had no timer) + groupInvitationManager1.respondToInvitation(contactId0From1, pg, true); + // Sync the accept response message to 0 + sync1To0(1, true); + // Sync the ack (and creator's join messages (group + protocol) to 1 + // this starts 1's timer + sync0To1(2, true); + waitForEvents(c1); + assertGroupCount(c0, contactId1From0, 2, 1); + assertGroupCount(c1, contactId0From1, 2, 0); + forEachHeader(c1, contactId0From1, 2, h -> { + if (h instanceof GroupInvitationRequest) { + // the request is for the first invitation + assertEquals(pg.getId(), + ((GroupInvitationRequest) h).getNameable().getId()); + } else { + assertTrue(h instanceof GroupInvitationResponse); + GroupInvitationResponse r = (GroupInvitationResponse) h; + // is accept for 1nd invitation + assertEquals(pg.getId(), r.getShareableId()); + assertFalse(r.isAutoDecline()); + assertTrue(r.wasAccepted()); + } + }); + + // Before 1's timer elapses, 1 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 2, 1); + assertEquals(2, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 2, 0); + assertEquals(2, getMessageHeaders(c1, contactId0From1).size()); + + // When 1's timer has elapsed, the message should be deleted from 1's + // view of the conversation + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 2, 1); + assertEquals(2, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 0); + forEachHeader(c1, contactId0From1, 1, h -> { + assertTrue(h instanceof GroupInvitationRequest); + assertTrue(((GroupInvitationRequest) h).wasAnswered()); + assertTrue(((GroupInvitationRequest) h).canBeOpened()); + }); + + // 0 reads all messages + forEachHeader(c0, contactId1From0, 2, h -> { + try { + if (!h.isRead()) markMessageRead(c0, contact1From0, h.getId()); + } catch (Exception e) { + fail(); + } + }); + assertGroupCount(c0, contactId1From0, 2, 0); + + // Before 0's timer elapses, 0 should still see the messages + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 2, 0); + assertGroupCount(c1, contactId0From1, 1, 0); + + // When 0's timer has elapsed, the messages should be deleted from 0's + // view of the conversation, only the initial invitation remains + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 0); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + + // 1 joined the PrivateGroup + assertEquals(pg, + c1.getPrivateGroupManager().getPrivateGroup(pg.getId())); + assertFalse(groupInvitationManager0 + .isInvitationAllowed(contact1From0, pg.getId())); + } + + @Test + public void testResponseAfterSenderDeletedInvitation() throws Exception { + setAutoDeleteTimer(c0, contact1From0.getId(), MIN_AUTO_DELETE_TIMER_MS); + + // Send invitation + sendInvitation(privateGroup, contact1From0.getId(), "Join this!"); + assertGroupCount(c0, contactId1From0, 1, 0); + + // Sync the message to 1 + sync0To1(1, true); + // Sync the ack to 0 - this starts 0's timer + ack1To0(1); + waitForEvents(c0); + assertGroupCount(c1, contactId0From1, 1, 1); + + // When 0's timer has elapsed, the message should be deleted from 0's + // view of the conversation but 1 should still see the message + long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS; + c0.getTimeTravel().addCurrentTimeMillis(timerLatency); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 1); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + + // 1 marks message as read - this starts 1's timer + markMessageRead(c1, contact0From1, + getMessageHeaders(c1, contactId0From1).get(0).getId()); + + // 1 responds to invitation + groupInvitationManager1 + .respondToInvitation(contactId0From1, privateGroup, false); + // Sync the decline response message to 0 + sync1To0(1, true); + // Sync the ack to 1 - this starts 1's timer + ack0To1(1); + waitForEvents(c1); + assertGroupCount(c0, contactId1From0, 1, 1); + assertGroupCount(c1, contactId0From1, 2, 0); + + // 0 marks the message as read - this starts 0's timer + GroupInvitationResponse message1 = (GroupInvitationResponse) + getMessageHeaders(c0, contactId1From0).get(0); + markMessageRead(c0, contact1From0, message1.getId()); + assertGroupCount(c0, contactId1From0, 1, 0); + assertGroupCount(c1, contactId0From1, 2, 0); + + // both peers delete all messages after their timers expire + c0.getTimeTravel().addCurrentTimeMillis(timerLatency); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 0, 0); + assertEquals(0, getMessageHeaders(c1, contactId0From1).size()); + } + + private PrivateGroup addPrivateGroup(String name, long timestamp) + throws DbException { + PrivateGroup pg = privateGroupFactory.createPrivateGroup(name, author0); + GroupMessage joinMsg0 = groupMessageFactory + .createJoinMessage(pg.getId(), timestamp, author0); + groupManager0.addPrivateGroup(pg, joinMsg0, true); + return pg; + } + + private void sendInvitation(PrivateGroup pg, ContactId contactId, + @Nullable String text) throws DbException { + DatabaseComponent db0 = c0.getDatabaseComponent(); + long timestamp = db0.transactionWithResult(true, txn -> + c0.getConversationManager() + .getTimestampForOutgoingMessage(txn, contactId)); + byte[] signature = groupInvitationFactory.signInvitation(contact1From0, + pg.getId(), timestamp, author0.getPrivateKey()); + long timer = getAutoDeleteTimer(c0, contactId, timestamp); + groupInvitationManager0.sendInvitation(pg.getId(), contactId, text, + timestamp, signature, timer); + } +} diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java index 4a3e7fb85..e47642e2e 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java @@ -123,19 +123,19 @@ public class CreatorProtocolEngineTest extends AbstractProtocolEngineTest { @Test public void testOnLeaveActionFromStart() throws Exception { CreatorSession session = getDefaultSession(START); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromDissolved() throws Exception { CreatorSession session = getDefaultSession(DISSOLVED); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromError() throws Exception { CreatorSession session = getDefaultSession(ERROR); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test @@ -143,7 +143,7 @@ public class CreatorProtocolEngineTest extends AbstractProtocolEngineTest { CreatorSession session = getDefaultSession(INVITED); expectOnLocalLeave(); - CreatorSession newSession = engine.onLeaveAction(txn, session); + CreatorSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(DISSOLVED, newSession.getState()); assertEquals(messageId, newSession.getLastLocalMessageId()); assertEquals(lastRemoteMessageId, newSession.getLastRemoteMessageId()); @@ -157,7 +157,7 @@ public class CreatorProtocolEngineTest extends AbstractProtocolEngineTest { CreatorSession session = getDefaultSession(JOINED); expectOnLocalLeave(); - CreatorSession newSession = engine.onLeaveAction(txn, session); + CreatorSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(DISSOLVED, newSession.getState()); assertEquals(messageId, newSession.getLastLocalMessageId()); assertEquals(lastRemoteMessageId, newSession.getLastRemoteMessageId()); @@ -171,7 +171,7 @@ public class CreatorProtocolEngineTest extends AbstractProtocolEngineTest { CreatorSession session = getDefaultSession(LEFT); expectOnLocalLeave(); - CreatorSession newSession = engine.onLeaveAction(txn, session); + CreatorSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(DISSOLVED, newSession.getState()); assertEquals(messageId, newSession.getLastLocalMessageId()); assertEquals(lastRemoteMessageId, newSession.getLastRemoteMessageId()); diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java index 3785a649e..7b873d897 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java @@ -21,6 +21,7 @@ import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.versioning.ClientVersioningManager; import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.bramble.test.DbExpectations; import org.briarproject.bramble.test.TestUtils; import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.SessionId; @@ -329,10 +330,11 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { context.checking(new Expectations() {{ oneOf(messageParser).parseMetadata(meta); will(returnValue(messageMetadata)); + oneOf(messageMetadata).getAutoDeleteTimer(); + will(returnValue(NO_AUTO_DELETE_TIMER)); oneOf(messageMetadata).getPrivateGroupId(); will(returnValue(privateGroup.getId())); }}); - } private void expectIncomingMessage(Role role, MessageType type) @@ -530,15 +532,13 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { public void testRespondToInvitationWithoutSession() throws Exception { SessionId sessionId = new SessionId(getRandomId()); - context.checking(new Expectations() {{ - oneOf(db).startTransaction(false); - will(returnValue(txn)); + context.checking(new DbExpectations() {{ + oneOf(db).transaction(with(false), withDbRunnable(txn)); oneOf(db).getContact(txn, contactId); will(returnValue(contact)); oneOf(contactGroupFactory).createContactGroup(CLIENT_ID, MAJOR_VERSION, contact); will(returnValue(contactGroup)); - oneOf(db).endTransaction(txn); }}); expectGetSession(noResults, sessionId, contactGroup.getId()); @@ -582,9 +582,8 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { private void expectRespondToInvitation(SessionId sessionId, boolean accept) throws Exception { expectGetSession(oneResult, sessionId, contactGroup.getId()); - context.checking(new Expectations() {{ - oneOf(db).startTransaction(false); - will(returnValue(txn)); + context.checking(new DbExpectations() {{ + oneOf(db).transaction(with(false), withDbRunnable(txn)); oneOf(db).getContact(txn, contactId); will(returnValue(contact)); oneOf(contactGroupFactory).createContactGroup(CLIENT_ID, @@ -594,10 +593,8 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { .parseInviteeSession(contactGroup.getId(), bdfSession); will(returnValue(inviteeSession)); if (accept) oneOf(inviteeEngine).onJoinAction(txn, inviteeSession); - else oneOf(inviteeEngine).onLeaveAction(txn, inviteeSession); + else oneOf(inviteeEngine).onLeaveAction(txn, inviteeSession, false); will(returnValue(inviteeSession)); - oneOf(db).commitTransaction(txn); - oneOf(db).endTransaction(txn); }}); expectStoreSession(inviteeSession, storageMessage.getId()); } @@ -656,10 +653,10 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { long time1 = 1L, time2 = 2L; MessageMetadata messageMetadata1 = new MessageMetadata(INVITE, privateGroup.getId(), time1, true, - true, true, false, true, NO_AUTO_DELETE_TIMER); + true, true, false, true, NO_AUTO_DELETE_TIMER, false); MessageMetadata messageMetadata2 = new MessageMetadata(JOIN, privateGroup.getId(), time2, true, - true, true, true, false, NO_AUTO_DELETE_TIMER); + true, true, true, false, NO_AUTO_DELETE_TIMER, false); InviteMessage invite = new InviteMessage(message.getId(), contactGroup.getId(), privateGroup.getId(), time1, "name", author, @@ -879,7 +876,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { oneOf(sessionParser) .parseCreatorSession(contactGroup.getId(), bdfSession); will(returnValue(creatorSession)); - oneOf(creatorEngine).onLeaveAction(txn, creatorSession); + oneOf(creatorEngine).onLeaveAction(txn, creatorSession, false); will(returnValue(creatorSession)); // session 2 oneOf(sessionParser).getRole(bdfSession2); @@ -887,7 +884,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { oneOf(sessionParser) .parseInviteeSession(contactGroup2.getId(), bdfSession2); will(returnValue(inviteeSession)); - oneOf(inviteeEngine).onLeaveAction(txn, inviteeSession); + oneOf(inviteeEngine).onLeaveAction(txn, inviteeSession, false); will(returnValue(inviteeSession)); // session 3 oneOf(sessionParser).getRole(bdfSession3); @@ -895,7 +892,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { oneOf(sessionParser) .parsePeerSession(contactGroup3.getId(), bdfSession3); will(returnValue(peerSession)); - oneOf(peerEngine).onLeaveAction(txn, peerSession); + oneOf(peerEngine).onLeaveAction(txn, peerSession, false); will(returnValue(peerSession)); }}); diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidatorTest.java index fd5ccbe1d..487380bdf 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidatorTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidatorTest.java @@ -636,8 +636,7 @@ public class GroupInvitationValidatorTest extends ValidatorTestCase { long autoDeleteTimer, BdfDictionary metadata) { context.checking(new Expectations() {{ oneOf(messageEncoder).encodeMetadata(type, message.getGroupId(), - message.getTimestamp(), false, false, false, false, false, - autoDeleteTimer); + message.getTimestamp(), autoDeleteTimer); will(returnValue(metadata)); }}); } diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java index 43e63f34f..47a749844 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java @@ -193,25 +193,25 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest { @Test public void testOnLeaveActionFromStart() throws Exception { InviteeSession session = getDefaultSession(START); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromLeft() throws Exception { InviteeSession session = getDefaultSession(LEFT); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromDissolved() throws Exception { InviteeSession session = getDefaultSession(DISSOLVED); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromError() throws Exception { InviteeSession session = getDefaultSession(ERROR); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test @@ -223,7 +223,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest { }}); InviteeSession session = getDefaultSession(INVITED); - InviteeSession newSession = engine.onLeaveAction(txn, session); + InviteeSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(START, newSession.getState()); assertSessionRecordedSentMessage(newSession); @@ -245,7 +245,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest { expectSendLeaveMessage(false); expectSetPrivateGroupVisibility(INVISIBLE); InviteeSession session = getDefaultSession(ACCEPTED); - InviteeSession newSession = engine.onLeaveAction(txn, session); + InviteeSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(LEFT, newSession.getState()); assertSessionRecordedSentMessage(newSession); @@ -257,7 +257,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest { expectSendLeaveMessage(false); expectSetPrivateGroupVisibility(INVISIBLE); InviteeSession session = getDefaultSession(JOINED); - InviteeSession newSession = engine.onLeaveAction(txn, session); + InviteeSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(LEFT, newSession.getState()); assertSessionRecordedSentMessage(newSession); diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java index 69383ba38..6118dd32c 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java @@ -145,31 +145,31 @@ public class PeerProtocolEngineTest extends AbstractProtocolEngineTest { @Test public void testOnLeaveActionFromStart() throws Exception { PeerSession session = getDefaultSession(START); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromAwaitMember() throws Exception { PeerSession session = getDefaultSession(AWAIT_MEMBER); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromNeitherJoined() throws Exception { PeerSession session = getDefaultSession(NEITHER_JOINED); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromLocalLeft() throws Exception { PeerSession session = getDefaultSession(LOCAL_LEFT); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromError() throws Exception { PeerSession session = getDefaultSession(ERROR); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test @@ -178,7 +178,7 @@ public class PeerProtocolEngineTest extends AbstractProtocolEngineTest { expectSendLeaveMessage(false); expectSetPrivateGroupVisibility(INVISIBLE); - PeerSession newSession = engine.onLeaveAction(txn, session); + PeerSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(NEITHER_JOINED, newSession.getState()); assertSessionRecordedSentMessage(newSession); @@ -191,7 +191,7 @@ public class PeerProtocolEngineTest extends AbstractProtocolEngineTest { expectSendLeaveMessage(false); expectSetPrivateGroupVisibility(INVISIBLE); - PeerSession newSession = engine.onLeaveAction(txn, session); + PeerSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(LOCAL_LEFT, newSession.getState()); assertSessionRecordedSentMessage(newSession); diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java index d0a4ce577..738b48e96 100644 --- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java +++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java @@ -28,6 +28,7 @@ import org.briarproject.briar.api.introduction.IntroductionManager; import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.PrivateMessageFactory; import org.briarproject.briar.api.privategroup.PrivateGroupManager; +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationFactory; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; import org.briarproject.briar.attachment.AttachmentModule; import org.briarproject.briar.autodelete.AutoDeleteModule; @@ -120,6 +121,8 @@ public interface BriarIntegrationTestComponent GroupInvitationManager getGroupInvitationManager(); + GroupInvitationFactory getGroupInvitationFactory(); + IntroductionManager getIntroductionManager(); MessageTracker getMessageTracker(); diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java index 8f6f97226..6d50b08ac 100644 --- a/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java +++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java @@ -51,7 +51,7 @@ public class BriarTestUtils { byte[] linkBytes = new byte[RAW_LINK_BYTES]; byte[] publicKey = keyPair.getPublic().getEncoded(); linkBytes[0] = FORMAT_VERSION; - arraycopy(publicKey,0, linkBytes, 1, RAW_LINK_BYTES - 1); + arraycopy(publicKey, 0, linkBytes, 1, RAW_LINK_BYTES - 1); return ("briar://" + Base32.encode(linkBytes)).toLowerCase(); }