diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java index 381c22a12..1b65ee41c 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java @@ -148,6 +148,10 @@ abstract class ProtocolEngineImpl localTimestamp, s.getLastLocalMessageId(), descriptor, text, timer); sendMessage(txn, m, INVITE, s.getShareableId(), 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(), localTimestamp, s.getLastLocalMessageId(), descriptor, @@ -216,6 +220,10 @@ abstract class ProtocolEngineImpl s.getShareableId(), localTimestamp, s.getLastLocalMessageId(), timer); sendMessage(txn, m, ACCEPT, s.getShareableId(), 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.encodeAcceptMessage(s.getContactGroupId(), s.getShareableId(), localTimestamp, @@ -271,6 +279,10 @@ abstract class ProtocolEngineImpl s.getShareableId(), localTimestamp, s.getLastLocalMessageId(), timer); sendMessage(txn, m, DECLINE, s.getShareableId(), true, timer); + // Set the auto-delete timer duration on the local message + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } } else { m = messageEncoder.encodeDeclineMessage(s.getContactGroupId(), s.getShareableId(), localTimestamp, diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java index 8ebfbadcf..0d20ef022 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java @@ -1,6 +1,7 @@ package org.briarproject.briar.sharing; 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; @@ -47,18 +49,20 @@ import java.util.Set; import javax.annotation.Nullable; 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.sharing.MessageType.ABORT; import static org.briarproject.briar.sharing.MessageType.ACCEPT; import static org.briarproject.briar.sharing.MessageType.DECLINE; import static org.briarproject.briar.sharing.MessageType.INVITE; import static org.briarproject.briar.sharing.MessageType.LEAVE; +import static org.briarproject.briar.sharing.State.LOCAL_INVITED; import static org.briarproject.briar.sharing.State.SHARING; @NotNullByDefault abstract class SharingManagerImpl extends ConversationClientImpl implements SharingManager, OpenDatabaseHook, ContactHook, - ClientVersioningHook { + ClientVersioningHook, CleanupHook { private final ClientVersioningManager clientVersioningManager; private final MessageParser messageParser; @@ -133,6 +137,11 @@ abstract class SharingManagerImpl BdfDictionary d) throws DbException, FormatException { // Parse the metadata MessageMetadata meta = messageParser.parseMetadata(d); + // 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.getShareableId()); StoredSession ss = getSession(txn, m.getGroupId(), sessionId); @@ -293,7 +302,11 @@ abstract class SharingManagerImpl @Override public void respondToInvitation(ContactId c, SessionId id, boolean accept) throws DbException { - Transaction txn = db.startTransaction(false); + db.transaction(false, txn -> respondToInvitation(txn, c, id, accept)); + } + + private void respondToInvitation(Transaction txn, ContactId c, + SessionId id, boolean accept) throws DbException { try { // Look up the session Contact contact = db.getContact(txn, c); @@ -308,11 +321,8 @@ abstract class SharingManagerImpl else session = engine.onDeclineAction(txn, session); // Store the updated session storeSession(txn, ss.storageId, session); - db.commitTransaction(txn); } catch (FormatException e) { throw new DbException(e); - } finally { - db.endTransaction(txn); } } @@ -573,6 +583,7 @@ abstract class SharingManagerImpl }, messageId -> false); } + @Override public DeletionResult deleteMessages(Transaction txn, ContactId c, Set messageIds) throws DbException { @@ -684,6 +695,56 @@ abstract class SharingManagerImpl 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.getShareableId()); + 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 + for (Entry entry : sessions.entrySet()) { + DeletableSession session = entry.getValue(); + // first decline pending shareable we're invited to + if (session.state == LOCAL_INVITED) { + respondToInvitation(txn, c, entry.getKey(), false); + } + 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/sharing/SharingModule.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingModule.java index 0bab71a49..96f6faed1 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingModule.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingModule.java @@ -1,5 +1,6 @@ package org.briarproject.briar.sharing; +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; @@ -75,7 +76,8 @@ public class SharingModule { ValidationManager validationManager, ConversationManager conversationManager, BlogManager blogManager, ClientVersioningManager clientVersioningManager, - BlogSharingManagerImpl blogSharingManager) { + BlogSharingManagerImpl blogSharingManager, + CleanupManager cleanupManager) { lifecycleManager.registerOpenDatabaseHook(blogSharingManager); contactManager.registerContactHook(blogSharingManager); validationManager.registerIncomingMessageHook( @@ -91,6 +93,9 @@ public class SharingModule { clientVersioningManager.registerClient(BlogManager.CLIENT_ID, BlogManager.MAJOR_VERSION, BlogManager.MINOR_VERSION, blogSharingManager.getShareableClientVersioningHook()); + cleanupManager.registerCleanupHook(BlogSharingManager.CLIENT_ID, + BlogSharingManager.MAJOR_VERSION, + blogSharingManager); return blogSharingManager; } @@ -134,7 +139,8 @@ public class SharingModule { ValidationManager validationManager, ConversationManager conversationManager, ForumManager forumManager, ClientVersioningManager clientVersioningManager, - ForumSharingManagerImpl forumSharingManager) { + ForumSharingManagerImpl forumSharingManager, + CleanupManager cleanupManager) { lifecycleManager.registerOpenDatabaseHook(forumSharingManager); contactManager.registerContactHook(forumSharingManager); validationManager.registerIncomingMessageHook( @@ -150,6 +156,9 @@ public class SharingModule { clientVersioningManager.registerClient(ForumManager.CLIENT_ID, ForumManager.MAJOR_VERSION, ForumManager.MINOR_VERSION, forumSharingManager.getShareableClientVersioningHook()); + cleanupManager.registerCleanupHook(ForumSharingManager.CLIENT_ID, + ForumSharingManager.MAJOR_VERSION, + forumSharingManager); return forumSharingManager; } diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/AbstractAutoDeleteIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/AbstractAutoDeleteIntegrationTest.java new file mode 100644 index 000000000..ee1d9c1aa --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/sharing/AbstractAutoDeleteIntegrationTest.java @@ -0,0 +1,161 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.sharing.InvitationResponse; +import org.briarproject.briar.api.sharing.Shareable; +import org.briarproject.briar.api.sharing.SharingManager; +import org.briarproject.briar.autodelete.AbstractAutoDeleteTest; + +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.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public abstract class AbstractAutoDeleteIntegrationTest + extends AbstractAutoDeleteTest { + + protected SharingManager sharingManager0; + protected Shareable shareable; + + protected void testAutoDeclinedSharing(SharingManager sharingManager0, + Shareable shareable) throws Exception { + setAutoDeleteTimer(c0, contactId1From0, MIN_AUTO_DELETE_TIMER_MS); + + assertGroupCount(c0, contactId1From0, 1, 0); + + // Send invitation + sharingManager0 + .sendInvitation(shareable.getId(), contactId1From0, + "This shareable!"); + + // 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 invitation message + sync0To1(1, true); + 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 InvitationResponse); + assertFalse(((InvitationResponse) h).wasAccepted()); + //TODO assertTrue(((InvitationResponse) 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(sharingManager0 + .canBeShared(shareable.getId(), contact1From0)); + + // 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(sharingManager0 + .canBeShared(shareable.getId(), contact1From0)); + // Send invitation + sharingManager0 + .sendInvitation(shareable.getId(), contactId1From0, + "This shareable, please be quick!"); + sync0To1(1, true); + assertGroupCount(c1, contactId0From1, 1, 1); + } +} diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/AutoDeleteBlogIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/AutoDeleteBlogIntegrationTest.java new file mode 100644 index 000000000..39665669c --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/sharing/AutoDeleteBlogIntegrationTest.java @@ -0,0 +1,31 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.briar.api.conversation.ConversationManager; +import org.briarproject.briar.test.BriarIntegrationTestComponent; +import org.junit.Before; +import org.junit.Test; + +public class AutoDeleteBlogIntegrationTest + extends AbstractAutoDeleteIntegrationTest { + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + // personalBlog(author0) is already shared with c1 + shareable = c0.getBlogManager().getPersonalBlog(author2); + sharingManager0 = c0.getBlogSharingManager(); + addContacts1And2(); + } + + @Override + protected ConversationManager.ConversationClient getConversationClient( + BriarIntegrationTestComponent component) { + return component.getBlogSharingManager(); + } + + @Test + public void testAutoDeclinedBlogSharing() throws Exception { + testAutoDeclinedSharing(sharingManager0, shareable); + } +} diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/AutoDeleteForumIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/AutoDeleteForumIntegrationTest.java new file mode 100644 index 000000000..322c71c4a --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/sharing/AutoDeleteForumIntegrationTest.java @@ -0,0 +1,32 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.briar.api.conversation.ConversationManager; +import org.briarproject.briar.api.forum.ForumManager; +import org.briarproject.briar.test.BriarIntegrationTestComponent; +import org.junit.Before; +import org.junit.Test; + +public class AutoDeleteForumIntegrationTest + extends AbstractAutoDeleteIntegrationTest { + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + ForumManager forumManager0 = c0.getForumManager(); + shareable = forumManager0.addForum("Test Forum"); + sharingManager0 = c0.getForumSharingManager(); + addContacts1And2(); + } + + @Override + protected ConversationManager.ConversationClient getConversationClient( + BriarIntegrationTestComponent component) { + return component.getForumSharingManager(); + } + + @Test + public void testAutoDeclinedForumSharing() throws Exception { + testAutoDeclinedSharing(sharingManager0, shareable); + } +}