diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java index 1d807dcddb16d697b458a3288af066a1785d4615..c585b6600bb10505be3fcd751b2efea491587d04 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java @@ -1020,13 +1020,13 @@ public class ConversationActivity extends BriarActivity @DatabaseExecutor private void respondToForumRequest(SessionId id, boolean accept) throws DbException { - forumSharingManager.respondToInvitation(id, accept); + forumSharingManager.respondToInvitation(contactId, id, accept); } @DatabaseExecutor private void respondToBlogRequest(SessionId id, boolean accept) throws DbException { - blogSharingManager.respondToInvitation(id, accept); + blogSharingManager.respondToInvitation(contactId, id, accept); } @DatabaseExecutor diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java index 6adad35b62b986ecf98e84d36cb9a8bd6836b5dd..39dcb24a4678430b686f1f56b374cedb982f6127 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java @@ -170,8 +170,7 @@ abstract class ConversationItem { } else if (ir instanceof GroupInvitationRequest) { text = ctx.getString( R.string.groups_invitations_invitation_sent, - contactName, - ((GroupInvitationRequest) ir).getGroupName()); + contactName, ir.getShareable().getName()); } else { throw new IllegalArgumentException("Unknown InvitationRequest"); } @@ -194,8 +193,7 @@ abstract class ConversationItem { } else if (ir instanceof GroupInvitationRequest) { text = ctx.getString( R.string.groups_invitations_invitation_received, - contactName, - ((GroupInvitationRequest) ir).getGroupName()); + contactName, ir.getShareable().getName()); type = GROUP; } else { throw new IllegalArgumentException("Unknown InvitationRequest"); @@ -203,7 +201,7 @@ abstract class ConversationItem { return new ConversationRequestItem(ir.getId(), ir.getGroupId(), type, ir.getSessionId(), text, ir.getMessage(), ir.getTimestamp(), ir.isRead(), - ir.getInvitedGroupId(), !ir.isAvailable(), + ir.getShareable().getId(), !ir.isAvailable(), ir.canBeOpened()); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java index 401b88ecb0ece5bbb766941e09097833f70f8f88..4f19691f7801dcc3ef2626bef7ee2ed8f1509cab 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java @@ -10,9 +10,11 @@ import org.briarproject.bramble.api.db.NoSuchGroupException; import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.system.Clock; import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl; import org.briarproject.briar.android.controller.handler.ExceptionHandler; import org.briarproject.briar.api.blog.BlogSharingManager; +import org.briarproject.briar.api.messaging.ConversationManager; import java.util.Collection; import java.util.concurrent.Executor; @@ -22,6 +24,7 @@ import javax.annotation.concurrent.Immutable; import javax.inject.Inject; import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @Immutable @NotNullByDefault @@ -31,14 +34,19 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl private final static Logger LOG = Logger.getLogger(ShareBlogControllerImpl.class.getName()); + private final ConversationManager conversationManager; private final BlogSharingManager blogSharingManager; + private final Clock clock; @Inject ShareBlogControllerImpl(@DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, ContactManager contactManager, - BlogSharingManager blogSharingManager) { + ConversationManager conversationManager, + BlogSharingManager blogSharingManager, Clock clock) { super(dbExecutor, lifecycleManager, contactManager); + this.conversationManager = conversationManager; this.blogSharingManager = blogSharingManager; + this.clock = clock; } @Override @@ -48,15 +56,19 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl @Override public void share(final GroupId g, final Collection<ContactId> contacts, - final String msg, + final String message, final ExceptionHandler<DbException> handler) { runOnDbThread(new Runnable() { @Override public void run() { try { + String msg = isNullOrEmpty(message) ? null : message; for (ContactId c : contacts) { try { - blogSharingManager.sendInvitation(g, c, msg); + long time = Math.max(clock.currentTimeMillis(), + conversationManager.getGroupCount(c) + .getLatestMsgTime() + 1); + blogSharingManager.sendInvitation(g, c, msg, time); } catch (NoSuchContactException | NoSuchGroupException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java index 93a16307b2717a856d085901b87d353969cd17d7..9770d3bc18d292759fda57bf4a4fe4d87bf84290 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java @@ -10,9 +10,11 @@ import org.briarproject.bramble.api.db.NoSuchGroupException; import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.system.Clock; import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl; import org.briarproject.briar.android.controller.handler.ExceptionHandler; import org.briarproject.briar.api.forum.ForumSharingManager; +import org.briarproject.briar.api.messaging.ConversationManager; import java.util.Collection; import java.util.concurrent.Executor; @@ -22,6 +24,7 @@ import javax.annotation.concurrent.Immutable; import javax.inject.Inject; import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @Immutable @NotNullByDefault @@ -31,14 +34,19 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl private final static Logger LOG = Logger.getLogger(ShareForumControllerImpl.class.getName()); + private final ConversationManager conversationManager; private final ForumSharingManager forumSharingManager; + private final Clock clock; @Inject ShareForumControllerImpl(@DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, ContactManager contactManager, - ForumSharingManager forumSharingManager) { + ConversationManager conversationManager, + ForumSharingManager forumSharingManager, Clock clock) { super(dbExecutor, lifecycleManager, contactManager); + this.conversationManager = conversationManager; this.forumSharingManager = forumSharingManager; + this.clock = clock; } @Override @@ -48,15 +56,19 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl @Override public void share(final GroupId g, final Collection<ContactId> contacts, - final String msg, + final String message, final ExceptionHandler<DbException> handler) { runOnDbThread(new Runnable() { @Override public void run() { try { + String msg = isNullOrEmpty(message) ? null : message; for (ContactId c : contacts) { try { - forumSharingManager.sendInvitation(g, c, msg); + long time = Math.max(clock.currentTimeMillis(), + conversationManager.getGroupCount(c) + .getLatestMsgTime() + 1); + forumSharingManager.sendInvitation(g, c, msg, time); } catch (NoSuchContactException | NoSuchGroupException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java index d0fabc9cb567257e5af283b1959f964b256bf358..0618f7b088b7c45c96fff92642d505f5e79c4706 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationRequest.java @@ -1,7 +1,10 @@ package org.briarproject.briar.api.blog; import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.client.SessionId; @@ -10,7 +13,7 @@ import org.briarproject.briar.api.sharing.InvitationRequest; import javax.annotation.Nullable; @NotNullByDefault -public class BlogInvitationRequest extends InvitationRequest { +public class BlogInvitationRequest extends InvitationRequest<Blog> { private final String blogAuthorName; @@ -19,9 +22,12 @@ public class BlogInvitationRequest extends InvitationRequest { @Nullable String message, GroupId blogId, boolean available, boolean canBeOpened, long time, boolean local, boolean sent, boolean seen, boolean read) { - - super(id, sessionId, groupId, contactId, message, blogId, available, - canBeOpened, time, local, sent, seen, read); + // TODO pass a proper blog here when redoing the BlogSharingManager + super(id, groupId, time, local, sent, seen, read, sessionId, + new Blog(new Group(blogId, BlogManager.CLIENT_ID, new byte[0]), + new Author(new AuthorId(new byte[AuthorId.LENGTH]), + blogAuthorName, new byte[0])), contactId, + message, available, canBeOpened); this.blogAuthorName = blogAuthorName; } 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 38a2e1680929bb1a532a111144161870868f1afb..32eaa44405fd5f69aacaa7d2db6f2a4e342d7276 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 @@ -14,9 +14,8 @@ public class BlogInvitationResponse extends InvitationResponse { GroupId groupId, ContactId contactId, GroupId blogId, boolean accept, long time, boolean local, boolean sent, boolean seen, boolean read) { - - super(id, sessionId, groupId, contactId, blogId, accept, time, local, - sent, seen, read); + super(id, groupId, time, local, sent, seen, read, sessionId, blogId, + contactId, accept); } } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java index 72c913c6f8163a4228561f2e1bc4edbefe401d69..2d23ab0e8bd768411a0b787dd8721d4076705fd8 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationRequest.java @@ -12,23 +12,18 @@ import javax.annotation.concurrent.Immutable; @Immutable @NotNullByDefault -public class ForumInvitationRequest extends InvitationRequest { - - private final String forumName; - - public ForumInvitationRequest(MessageId id, SessionId sessionId, - GroupId groupId, ContactId contactId, GroupId forumId, - String forumName, @Nullable String message, boolean available, - boolean canBeOpened, long time, boolean local, boolean sent, - boolean seen, boolean read) { - - super(id, sessionId, groupId, contactId, message, forumId, available, - canBeOpened, time, local, sent, seen, read); - this.forumName = forumName; +public class ForumInvitationRequest extends InvitationRequest<Forum> { + + public ForumInvitationRequest(MessageId id, GroupId groupId, long time, + boolean local, boolean sent, boolean seen, boolean read, + SessionId sessionId, Forum forum, ContactId contactId, + @Nullable String message, boolean available, boolean canBeOpened) { + super(id, groupId, time, local, sent, seen, read, sessionId, forum, + contactId, message, available, canBeOpened); } public String getForumName() { - return forumName; + return getShareable().getName(); } } 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 31eb8698ab8fff6fcd288b59b1f0e802b84915b0..48f03ce8593e4874ed550c71400b3d1df299b052 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 @@ -13,13 +13,12 @@ import javax.annotation.concurrent.Immutable; @NotNullByDefault public class ForumInvitationResponse extends InvitationResponse { - public ForumInvitationResponse(MessageId id, SessionId sessionId, - GroupId groupId, ContactId contactId, GroupId forumId, - boolean accept, long time, boolean local, boolean sent, - boolean seen, boolean read) { - - super(id, sessionId, groupId, contactId, forumId, accept, time, local, - sent, seen, read); + public ForumInvitationResponse(MessageId id, GroupId groupId, long time, + boolean local, boolean sent, boolean seen, boolean read, + SessionId sessionId, GroupId forumId, ContactId contactId, + boolean accept) { + super(id, groupId, time, local, sent, seen, read, sessionId, forumId, + contactId, accept); } } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java index f00a926af45f21464641057b5199c68876c72bd8..c861b4e63d72eb84c5dcae9f02e5a02353344826 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java @@ -27,6 +27,11 @@ public interface ForumManager { */ Forum addForum(String name) throws DbException; + /** + * Subscribes to a forum within the given {@link Transaction}. + */ + void addForum(Transaction txn, Forum f) throws DbException; + /** * Unsubscribes from a forum. */ diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumInvitationResponseReceivedEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumInvitationResponseReceivedEvent.java index 60567907a19efa601d933126f2f187c09c07caa7..460ea3cab255423a4ff1eb9da737a09558da2f0c 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumInvitationResponseReceivedEvent.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/event/ForumInvitationResponseReceivedEvent.java @@ -1,21 +1,20 @@ package org.briarproject.briar.api.forum.event; import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.api.forum.ForumInvitationResponse; import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault public class ForumInvitationResponseReceivedEvent extends InvitationResponseReceivedEvent { - private final String forumName; - - public ForumInvitationResponseReceivedEvent(String forumName, - ContactId contactId, ForumInvitationResponse response) { + public ForumInvitationResponseReceivedEvent(ContactId contactId, + ForumInvitationResponse response) { super(contactId, response); - this.forumName = forumName; } - public String getForumName() { - return forumName; - } } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java index 9ebdb224b6d93bdbf3d7026327917b5e126940ff..d447ae8ef176629db9ed8b25e901fd0ab4c633d2 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationRequest.java @@ -1,11 +1,11 @@ package org.briarproject.briar.api.privategroup.invitation; import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.identity.Author; 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.privategroup.PrivateGroup; import org.briarproject.briar.api.sharing.InvitationRequest; import javax.annotation.Nullable; @@ -13,28 +13,14 @@ import javax.annotation.concurrent.Immutable; @Immutable @NotNullByDefault -public class GroupInvitationRequest extends InvitationRequest { - - private final String groupName; - private final Author creator; - - public GroupInvitationRequest(MessageId id, SessionId sessionId, - GroupId groupId, ContactId contactId, @Nullable String message, - GroupId privateGroupId, String groupName, Author creator, - boolean available, boolean canBeOpened, long time, - boolean local, boolean sent, boolean seen, boolean read) { - super(id, sessionId, groupId, contactId, message, privateGroupId, - available, canBeOpened, time, local, sent, seen, read); - this.groupName = groupName; - this.creator = creator; - } - - public String getGroupName() { - return groupName; - } - - public Author getCreator() { - return creator; +public class GroupInvitationRequest extends InvitationRequest<PrivateGroup> { + + public GroupInvitationRequest(MessageId id, GroupId groupId, long time, + boolean local, boolean sent, boolean seen, boolean read, + SessionId sessionId, PrivateGroup shareable, ContactId contactId, + @Nullable String message, boolean available, boolean canBeOpened) { + super(id, groupId, time, local, sent, seen, read, sessionId, shareable, + contactId, message, available, canBeOpened); } } 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 2f89e1e85b7e6f8ba45811e65bb93c79393f26cb..b2e7d54b538cc3ba3bb5590e5c834a654280af78 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 @@ -13,11 +13,12 @@ import javax.annotation.concurrent.Immutable; @NotNullByDefault public class GroupInvitationResponse extends InvitationResponse { - public GroupInvitationResponse(MessageId id, SessionId sessionId, - GroupId groupId, ContactId contactId, GroupId privateGroupId, - boolean accept, long time, boolean local, boolean sent, - boolean seen, boolean read) { - super(id, sessionId, groupId, contactId, privateGroupId, accept, time, - local, sent, seen, read); + public GroupInvitationResponse(MessageId id, GroupId groupId, long time, + boolean local, boolean sent, boolean seen, boolean read, + SessionId sessionId, GroupId shareableId, ContactId contactId, + boolean accept) { + super(id, groupId, time, local, sent, seen, read, sessionId, + shareableId, contactId, accept); } + } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationFactory.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationFactory.java index eab12f74142f15539b4f8f9a2dae4c5fa93a0a9f..5b4e275a3958588e71a66d725f06aef9474ac332 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationFactory.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationFactory.java @@ -4,6 +4,7 @@ import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.sync.GroupId; +@Deprecated public interface InvitationFactory<I extends SharingMessage.Invitation> { I build(GroupId groupId, BdfDictionary d) throws FormatException; diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java index 7dd97d0503b8596075010540cf0059225d0f3871..4bdc394559767cdd4603ba07d39659bc81111835 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationMessage.java @@ -7,25 +7,22 @@ import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.client.BaseMessageHeader; import org.briarproject.briar.api.client.SessionId; -import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @Immutable @NotNullByDefault -public abstract class InvitationMessage extends BaseMessageHeader { +public class InvitationMessage extends BaseMessageHeader { private final SessionId sessionId; private final ContactId contactId; - private final GroupId invitedGroupId; - public InvitationMessage(MessageId id, SessionId sessionId, GroupId groupId, - ContactId contactId, GroupId invitedGroupId, long time, - boolean local, boolean sent, boolean seen, boolean read) { + public InvitationMessage(MessageId id, GroupId groupId, long time, + boolean local, boolean sent, boolean seen, boolean read, + SessionId sessionId, ContactId contactId) { super(id, groupId, time, local, read, sent, seen); this.sessionId = sessionId; this.contactId = contactId; - this.invitedGroupId = invitedGroupId; } public SessionId getSessionId() { @@ -36,9 +33,4 @@ public abstract class InvitationMessage extends BaseMessageHeader { return contactId; } - @Nullable - public GroupId getInvitedGroupId() { - return invitedGroupId; - } - } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java index 9acc01e7ca5893f6c0ce31c73c7cd26bbf255e64..c5239310d467872bc182c8b17c2471d7924506ee 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationRequest.java @@ -11,20 +11,19 @@ import javax.annotation.concurrent.Immutable; @Immutable @NotNullByDefault -public abstract class InvitationRequest extends InvitationMessage { +public class InvitationRequest<S extends Shareable> extends InvitationMessage { + private final S shareable; @Nullable private final String message; private final boolean available, canBeOpened; - public InvitationRequest(MessageId id, SessionId sessionId, GroupId groupId, - ContactId contactId, @Nullable String message, - GroupId invitedGroupId, boolean available, - boolean canBeOpened, long time, boolean local, boolean sent, - boolean seen, boolean read) { - super(id, sessionId, groupId, contactId, invitedGroupId, time, local, - sent, seen, read); - if (available && canBeOpened) throw new IllegalArgumentException(); + public InvitationRequest(MessageId id, GroupId groupId, long time, + boolean local, boolean sent, boolean seen, boolean read, + SessionId sessionId, S shareable, ContactId contactId, + @Nullable String message, boolean available, boolean canBeOpened) { + super(id, groupId, time, local, sent, seen, read, sessionId, contactId); + this.shareable = shareable; this.message = message; this.available = available; this.canBeOpened = canBeOpened; @@ -43,4 +42,8 @@ public abstract class InvitationRequest extends InvitationMessage { return canBeOpened; } + public S getShareable() { + return shareable; + } + } 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 248e443ffd2cba3528d0d2215bb22f88f9779347..517b6068e96c0bd30aaf62fdae98671a93ff1fab 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 @@ -10,21 +10,26 @@ import javax.annotation.concurrent.Immutable; @Immutable @NotNullByDefault -public abstract class InvitationResponse extends InvitationMessage { +public class InvitationResponse extends InvitationMessage { + private final GroupId shareableId; private final boolean accept; - public InvitationResponse(MessageId id, SessionId sessionId, - GroupId groupId, ContactId contactId, - GroupId invitedGroupId, boolean accept, long time, - boolean local, boolean sent, boolean seen, boolean read) { - - super(id, sessionId, groupId, contactId, invitedGroupId, time, local, - sent, seen, read); + public InvitationResponse(MessageId id, GroupId groupId, + long time, boolean local, boolean sent, boolean seen, + boolean read, SessionId sessionId, GroupId shareableId, + ContactId contactId, boolean accept) { + super(id, groupId, time, local, sent, seen, read, sessionId, contactId); + this.shareableId = shareableId; this.accept = accept; } public boolean wasAccepted() { return accept; } + + public GroupId getShareableId() { + return shareableId; + } + } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingConstants.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingConstants.java index 53f4557e45e3ff86702d55fb0d6aba2ad6d84986..6b8fcc4a058a1fcbb1b6708a51a3ec97f108d617 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingConstants.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingConstants.java @@ -15,34 +15,63 @@ public interface SharingConstants { */ int MAX_INVITATION_MESSAGE_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024; + @Deprecated String CONTACT_ID = "contactId"; + @Deprecated String GROUP_ID = "groupId"; + @Deprecated String TO_BE_SHARED_BY_US = "toBeSharedByUs"; + @Deprecated String SHARED_BY_US = "sharedByUs"; + @Deprecated String SHARED_WITH_US = "sharedWithUs"; + @Deprecated String TYPE = "type"; + @Deprecated String SESSION_ID = "sessionId"; + @Deprecated String STORAGE_ID = "storageId"; + @Deprecated String STATE = "state"; + @Deprecated String LOCAL = "local"; + @Deprecated String TIME = "time"; + @Deprecated String IS_SHARER = "isSharer"; + @Deprecated String SHAREABLE_ID = "shareableId"; + @Deprecated String INVITATION_MSG = "invitationMsg"; + @Deprecated String INVITATION_ID = "invitationId"; + @Deprecated String RESPONSE_ID = "responseId"; + @Deprecated int SHARE_MSG_TYPE_INVITATION = 1; + @Deprecated int SHARE_MSG_TYPE_ACCEPT = 2; + @Deprecated int SHARE_MSG_TYPE_DECLINE = 3; + @Deprecated int SHARE_MSG_TYPE_LEAVE = 4; + @Deprecated int SHARE_MSG_TYPE_ABORT = 5; + @Deprecated int TASK_ADD_SHAREABLE_TO_LIST_SHARED_WITH_US = 0; + @Deprecated int TASK_REMOVE_SHAREABLE_FROM_LIST_SHARED_WITH_US = 1; + @Deprecated int TASK_ADD_SHARED_SHAREABLE = 2; + @Deprecated int TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US = 3; + @Deprecated int TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US = 4; + @Deprecated int TASK_SHARE_SHAREABLE = 5; + @Deprecated int TASK_UNSHARE_SHAREABLE_SHARED_BY_US = 6; + @Deprecated int TASK_UNSHARE_SHAREABLE_SHARED_WITH_US = 7; } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java index 693c0f8b4204a2e3b59e65f011844f2033c4d4f7..b593d2fdf74207abe5be1f1f7216b67cfaae7154 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java @@ -20,8 +20,8 @@ public interface SharingManager<S extends Shareable> * Sends an invitation to share the given group with the given contact * and sends an optional message along with it. */ - void sendInvitation(GroupId groupId, ContactId contactId, - @Nullable String message) throws DbException; + void sendInvitation(GroupId shareableId, ContactId contactId, + @Nullable String message, long timestamp) throws DbException; /** * Responds to a pending group invitation @@ -32,7 +32,7 @@ public interface SharingManager<S extends Shareable> /** * Responds to a pending group invitation */ - void respondToInvitation(SessionId id, boolean accept) + void respondToInvitation(ContactId c, SessionId id, boolean accept) throws DbException; /** diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingMessage.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingMessage.java index 23340ac1c012f35a61407bfbc8db4e06279b8865..dd4b987aa95d6b3283632c147b3980f350bf00d3 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingMessage.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingMessage.java @@ -21,6 +21,7 @@ import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE import static org.briarproject.briar.api.sharing.SharingConstants.TIME; import static org.briarproject.briar.api.sharing.SharingConstants.TYPE; +@Deprecated @NotNullByDefault public interface SharingMessage { diff --git a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java index 348fe52100da5209d383b51c48e95f424c09d6a6..fef8224bdd2c01b08bc02db6531b1232bae8d463 100644 --- a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java @@ -108,6 +108,11 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager { return f; } + @Override + public void addForum(Transaction txn, Forum f) throws DbException { + db.addGroup(txn, f.getGroup()); + } + @Override public void removeForum(Forum f) throws DbException { Transaction txn = db.startTransaction(false); 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 36c00ffb1000a65e54810ac48187b43380d798af..30c1401197607a10ad93dba778f84d49860f0177 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 @@ -4,7 +4,6 @@ import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.Transaction; @@ -177,7 +176,7 @@ abstract class AbstractProtocolEngine<S extends Session> void subscribeToPrivateGroup(Transaction txn, MessageId inviteId) throws DbException, FormatException { - InviteMessage invite = getInviteMessage(txn, inviteId); + InviteMessage invite = messageParser.getInviteMessage(txn, inviteId); PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup( invite.getGroupName(), invite.getCreator(), invite.getSalt()); long timestamp = @@ -197,14 +196,6 @@ abstract class AbstractProtocolEngine<S extends Session> session.getInviteTimestamp()) + 1); } - private InviteMessage getInviteMessage(Transaction txn, MessageId m) - throws DbException, FormatException { - Message message = clientHelper.getMessage(txn, m); - if (message == null) throw new DbException(); - BdfList body = clientHelper.toList(message); - return messageParser.parseInviteMessage(message, body); - } - private void sendMessage(Transaction txn, Message m, MessageType type, GroupId privateGroupId, boolean visibleInConversation) throws DbException { 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 312957b43193ff63bed4daffbcdab91c8a472ddf..ad550742ff332c6625837bef767a7d18001e217e 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 @@ -254,8 +254,8 @@ class CreatorProtocolEngine extends AbstractProtocolEngine<CreatorSession> { private GroupInvitationResponse createInvitationResponse( GroupInvitationMessage m, ContactId c, boolean accept) { SessionId sessionId = new SessionId(m.getPrivateGroupId().getBytes()); - return new GroupInvitationResponse(m.getId(), sessionId, - m.getContactGroupId(), c, m.getPrivateGroupId(), accept, - m.getTimestamp(), false, false, true, false); + return new GroupInvitationResponse(m.getId(), m.getContactGroupId(), + m.getTimestamp(), false, false, true, false, sessionId, + m.getPrivateGroupId(), c, accept); } } 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 5570ffe41198db078cb978f59cd83e8a323dad26..1fac77c630aa1e049776b518c0217df1f7f0f141 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 @@ -71,7 +71,6 @@ class GroupInvitationManagerImpl extends ConversationClientImpl private final ProtocolEngine<CreatorSession> creatorEngine; private final ProtocolEngine<InviteeSession> inviteeEngine; private final ProtocolEngine<PeerSession> peerEngine; - private final Group localGroup; @Inject GroupInvitationManagerImpl(DatabaseComponent db, @@ -93,12 +92,10 @@ class GroupInvitationManagerImpl extends ConversationClientImpl creatorEngine = engineFactory.createCreatorEngine(); inviteeEngine = engineFactory.createInviteeEngine(); peerEngine = engineFactory.createPeerEngine(); - localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID); } @Override public void createLocalState(Transaction txn) throws DbException { - db.addGroup(txn, localGroup); // Ensure we've set things up for any pre-existing contacts for (Contact c : db.getContacts(txn)) addingContact(txn, c); } @@ -404,22 +401,15 @@ class GroupInvitationManagerImpl extends ConversationClientImpl throws DbException, FormatException { SessionId sessionId = getSessionId(meta.getPrivateGroupId()); // Look up the invite message to get the details of the private group - InviteMessage invite = getInviteMessage(txn, m); + InviteMessage invite = messageParser.getInviteMessage(txn, m); + PrivateGroup pg = privateGroupFactory + .createPrivateGroup(invite.getGroupName(), invite.getCreator(), + invite.getSalt()); boolean canBeOpened = db.containsGroup(txn, invite.getPrivateGroupId()); - return new GroupInvitationRequest(m, sessionId, contactGroupId, c, - invite.getMessage(), invite.getPrivateGroupId(), - invite.getGroupName(), invite.getCreator(), - meta.isAvailableToAnswer(), canBeOpened, meta.getTimestamp(), - meta.isLocal(), status.isSent(), status.isSeen(), - meta.isRead()); - } - - private InviteMessage getInviteMessage(Transaction txn, MessageId m) - throws DbException, FormatException { - Message message = clientHelper.getMessage(txn, m); - if (message == null) throw new DbException(); - BdfList body = clientHelper.toList(message); - return messageParser.parseInviteMessage(message, body); + return new GroupInvitationRequest(m, contactGroupId, + meta.getTimestamp(), meta.isLocal(), status.isSent(), + status.isSeen(), meta.isRead(), sessionId, pg, c, + invite.getMessage(), meta.isAvailableToAnswer(), canBeOpened); } private GroupInvitationResponse parseInvitationResponse(ContactId c, @@ -427,10 +417,10 @@ class GroupInvitationManagerImpl extends ConversationClientImpl MessageStatus status, boolean accept) throws DbException, FormatException { SessionId sessionId = getSessionId(meta.getPrivateGroupId()); - return new GroupInvitationResponse(m, sessionId, contactGroupId, c, - meta.getPrivateGroupId(), accept, meta.getTimestamp(), - meta.isLocal(), status.isSent(), status.isSeen(), - meta.isRead()); + return new GroupInvitationResponse(m, contactGroupId, + meta.getTimestamp(), meta.isLocal(), status.isSent(), + status.isSeen(), meta.isRead(), sessionId, + meta.getPrivateGroupId(), c, accept); } @Override @@ -481,7 +471,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl private GroupInvitationItem parseGroupInvitationItem(Transaction txn, Contact c, MessageId m) throws DbException, FormatException { - InviteMessage invite = getInviteMessage(txn, m); + InviteMessage invite = messageParser.getInviteMessage(txn, m); PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup( invite.getGroupName(), invite.getCreator(), invite.getSalt()); return new GroupInvitationItem(privateGroup, c); 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 e9dcac50860d4d7ed40bb0b04674c2716449c526..bf30f96edd34a182c3f7f3e8b7c8682427736a74 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 @@ -234,8 +234,9 @@ class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> { // Broadcast an event PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup( m.getGroupName(), m.getCreator(), m.getSalt()); - txn.attach(new GroupInvitationRequestReceivedEvent(privateGroup, - contactId, createInvitationRequest(m, contactId))); + txn.attach( + new GroupInvitationRequestReceivedEvent(privateGroup, contactId, + createInvitationRequest(m, privateGroup, contactId))); // Move to the INVITED state return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(), s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), @@ -317,11 +318,11 @@ class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> { } private GroupInvitationRequest createInvitationRequest(InviteMessage m, - ContactId c) { + PrivateGroup pg, ContactId c) { SessionId sessionId = new SessionId(m.getPrivateGroupId().getBytes()); - return new GroupInvitationRequest(m.getId(), sessionId, - m.getContactGroupId(), c, m.getMessage(), m.getPrivateGroupId(), - m.getGroupName(), m.getCreator(), true, false, m.getTimestamp(), - false, false, true, false); + return new GroupInvitationRequest(m.getId(), m.getContactGroupId(), + m.getTimestamp(), false, false, true, false, sessionId, pg, c, + m.getMessage(), true, false); } + } diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParser.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParser.java index 9e6a7ad9dbeb423a32e526983162a46f7a829c8e..8300b305c8c8ba7d299a40694aa56f5cfa85026d 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParser.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParser.java @@ -3,9 +3,12 @@ package org.briarproject.briar.privategroup.invitation; import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; @NotNullByDefault interface MessageParser { @@ -18,6 +21,9 @@ interface MessageParser { MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException; + InviteMessage getInviteMessage(Transaction txn, MessageId m) + throws DbException, FormatException; + InviteMessage parseInviteMessage(Message m, BdfList body) throws FormatException; 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 b31ff7d3174240e7a1c85d1edab0f06d288fb702..1cf35ee9c32b740382678c5bdcdcb7de473c678e 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 @@ -1,9 +1,12 @@ package org.briarproject.briar.privategroup.invitation; import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfEntry; import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.AuthorFactory; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; @@ -31,12 +34,14 @@ class MessageParserImpl implements MessageParser { private final AuthorFactory authorFactory; private final PrivateGroupFactory privateGroupFactory; + private final ClientHelper clientHelper; @Inject MessageParserImpl(AuthorFactory authorFactory, - PrivateGroupFactory privateGroupFactory) { + PrivateGroupFactory privateGroupFactory, ClientHelper clientHelper) { this.authorFactory = authorFactory; this.privateGroupFactory = privateGroupFactory; + this.clientHelper = clientHelper; } @Override @@ -78,6 +83,15 @@ class MessageParserImpl implements MessageParser { visible, available); } + @Override + public InviteMessage getInviteMessage(Transaction txn, MessageId m) + throws DbException, FormatException { + Message message = clientHelper.getMessage(txn, m); + if (message == null) throw new DbException(); + BdfList body = clientHelper.toList(message); + return parseInviteMessage(message, body); + } + @Override public InviteMessage parseInviteMessage(Message m, BdfList body) throws FormatException { diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/AbortMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/AbortMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..188c0b5013aa017336a2ace5aeb3a769844784b5 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/AbortMessage.java @@ -0,0 +1,19 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class AbortMessage extends SharingMessage { + + AbortMessage(MessageId id, GroupId contactGroupId, GroupId shareableId, + long timestamp, @Nullable MessageId previousMessageId) { + super(id, contactGroupId, shareableId, timestamp, previousMessageId); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/AcceptMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/AcceptMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0a440a5cd2bccbba67992306f523af925914c589 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/AcceptMessage.java @@ -0,0 +1,19 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class AcceptMessage extends SharingMessage { + + AcceptMessage(MessageId id, @Nullable MessageId previousMessageId, + GroupId contactGroupId, GroupId shareableId, long timestamp) { + super(id, contactGroupId, shareableId, timestamp, previousMessageId); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java index 3b0c0d9846f96869c9f2c943c6dd687433d4a724..a436dd9a39bb7c3ae826fb21a3b3f52b3fd68be7 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java @@ -52,7 +52,7 @@ import static org.briarproject.briar.api.sharing.SharingConstants.RESPONSE_ID; @Immutable @NotNullByDefault class BlogSharingManagerImpl extends - SharingManagerImpl<Blog, BlogInvitation, BlogInviteeSessionState, BlogSharerSessionState, BlogInvitationRequestReceivedEvent, BlogInvitationResponseReceivedEvent> + OldSharingManagerImpl<Blog, BlogInvitation, BlogInviteeSessionState, BlogSharerSessionState, BlogInvitationRequestReceivedEvent, BlogInvitationResponseReceivedEvent> implements BlogSharingManager, RemoveBlogHook { private final ContactManager contactManager; @@ -159,7 +159,7 @@ class BlogSharingManagerImpl extends } @Override - protected InvitationFactory<BlogInvitation, BlogSharerSessionState> getIFactory() { + protected OldInvitationFactory<BlogInvitation, BlogSharerSessionState> getIFactory() { return iFactory; } @@ -251,7 +251,7 @@ class BlogSharingManagerImpl extends } private static class IFactory implements - InvitationFactory<BlogInvitation, BlogSharerSessionState> { + OldInvitationFactory<BlogInvitation, BlogSharerSessionState> { @Override public BlogInvitation build(GroupId groupId, BdfDictionary d) throws FormatException { diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/DeclineMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/DeclineMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..65f9a9fe12d00458d506bdd75fb78e713c3693ea --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/DeclineMessage.java @@ -0,0 +1,20 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class DeclineMessage extends SharingMessage { + + DeclineMessage(MessageId id, GroupId contactGroupId, + GroupId shareableId, long timestamp, + @Nullable MessageId previousMessageId) { + super(id, contactGroupId, shareableId, timestamp, previousMessageId); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInvitationFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInvitationFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..591f25bc97af0dc93dc956e5d5943a2369d21d24 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInvitationFactoryImpl.java @@ -0,0 +1,39 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.contact.ContactId; +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.forum.Forum; +import org.briarproject.briar.api.forum.ForumInvitationRequest; +import org.briarproject.briar.api.forum.ForumInvitationResponse; + +import javax.inject.Inject; + +public class ForumInvitationFactoryImpl implements InvitationFactory<Forum> { + + @Inject + ForumInvitationFactoryImpl() { + } + + @Override + public ForumInvitationRequest createInvitationRequest(boolean local, + boolean sent, boolean seen, boolean read, InviteMessage<Forum> m, + ContactId c, boolean available, boolean canBeOpened) { + SessionId sessionId = new SessionId(m.getShareableId().getBytes()); + return new ForumInvitationRequest(m.getId(), m.getContactGroupId(), + m.getTimestamp(), local, sent, seen, read, sessionId, + m.getShareable(), c, m.getMessage(), available, canBeOpened); + } + + @Override + public ForumInvitationResponse createInvitationResponse(MessageId id, + GroupId contactGroupId, long time, boolean local, boolean sent, + boolean seen, boolean read, GroupId shareableId, + ContactId contactId, boolean accept) { + SessionId sessionId = new SessionId(shareableId.getBytes()); + return new ForumInvitationResponse(id, contactGroupId, time, local, + sent, seen, read, sessionId, shareableId, contactId, accept); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInviteeSessionState.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInviteeSessionState.java deleted file mode 100644 index d6a462982fd615cafecea793c08bbe02104952c9..0000000000000000000000000000000000000000 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumInviteeSessionState.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.briarproject.briar.sharing; - -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.data.BdfDictionary; -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 javax.annotation.concurrent.NotThreadSafe; - -import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME; -import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT; - -@NotThreadSafe -@NotNullByDefault -class ForumInviteeSessionState extends InviteeSessionState { - - private final String forumName; - private final byte[] forumSalt; - - ForumInviteeSessionState(SessionId sessionId, MessageId storageId, - GroupId groupId, State state, ContactId contactId, GroupId forumId, - String forumName, byte[] forumSalt, MessageId invitationId) { - super(sessionId, storageId, groupId, state, contactId, forumId, - invitationId); - - this.forumName = forumName; - this.forumSalt = forumSalt; - } - - @Override - public BdfDictionary toBdfDictionary() { - BdfDictionary d = super.toBdfDictionary(); - d.put(FORUM_NAME, getForumName()); - d.put(FORUM_SALT, getForumSalt()); - return d; - } - - String getForumName() { - return forumName; - } - - byte[] getForumSalt() { - return forumSalt; - } -} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumMessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumMessageParserImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..ed1ca425cf76bd504a0421f0bf1b04cd0712de3b --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumMessageParserImpl.java @@ -0,0 +1,34 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.api.forum.Forum; +import org.briarproject.briar.api.forum.ForumFactory; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +@Immutable +@NotNullByDefault +class ForumMessageParserImpl extends MessageParserImpl<Forum> { + + private final ForumFactory forumFactory; + + @Inject + ForumMessageParserImpl(ClientHelper clientHelper, + ForumFactory forumFactory) { + super(clientHelper); + this.forumFactory = forumFactory; + } + + @Override + protected Forum createShareable(BdfList descriptor) + throws FormatException { + String name = descriptor.getString(0); + byte[] salt = descriptor.getRaw(1); + return forumFactory.createForum(name, salt); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumProtocolEngineImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumProtocolEngineImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..0dd97300b04208ef861fea585f3682dfc50377ea --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumProtocolEngineImpl.java @@ -0,0 +1,93 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +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.Transaction; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.ClientId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.forum.Forum; +import org.briarproject.briar.api.forum.ForumInvitationRequest; +import org.briarproject.briar.api.forum.ForumInvitationResponse; +import org.briarproject.briar.api.forum.ForumManager; +import org.briarproject.briar.api.forum.ForumSharingManager; +import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent; +import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +@Immutable +@NotNullByDefault +class ForumProtocolEngineImpl extends ProtocolEngineImpl<Forum> { + + private final ForumManager forumManager; + private final InvitationFactory<Forum> invitationFactory; + + @Inject + ForumProtocolEngineImpl(DatabaseComponent db, + ClientHelper clientHelper, MessageEncoder messageEncoder, + MessageParser<Forum> messageParser, MessageTracker messageTracker, + Clock clock, ForumManager forumManager, + InvitationFactory<Forum> invitationFactory) { + super(db, clientHelper, messageEncoder, messageParser, messageTracker, + clock); + this.forumManager = forumManager; + this.invitationFactory = invitationFactory; + } + + @Override + Event getInvitationRequestReceivedEvent(InviteMessage<Forum> m, + ContactId contactId, boolean available, boolean canBeOpened) { + ForumInvitationRequest request = + (ForumInvitationRequest) invitationFactory + .createInvitationRequest(false, false, true, false, m, + contactId, available, canBeOpened); + return new ForumInvitationRequestReceivedEvent(m.getShareable(), + contactId, request); + } + + @Override + Event getInvitationResponseReceivedEvent(AcceptMessage m, + ContactId contactId) { + ForumInvitationResponse response = + (ForumInvitationResponse) invitationFactory + .createInvitationResponse(m.getId(), + m.getContactGroupId(), m.getTimestamp(), false, + false, true, false, m.getShareableId(), + contactId, true); + return new ForumInvitationResponseReceivedEvent(contactId, response); + } + + @Override + Event getInvitationResponseReceivedEvent(DeclineMessage m, + ContactId contactId) { + ForumInvitationResponse response = + (ForumInvitationResponse) invitationFactory + .createInvitationResponse(m.getId(), + m.getContactGroupId(), m.getTimestamp(), false, + false, true, false, m.getShareableId(), + contactId, true); + return new ForumInvitationResponseReceivedEvent(contactId, response); + } + + @Override + protected ClientId getClientId() { + return ForumSharingManager.CLIENT_ID; + } + + @Override + protected void addShareable(Transaction txn, MessageId inviteId) + throws DbException, FormatException { + InviteMessage<Forum> invite = + messageParser.getInviteMessage(txn, inviteId); + forumManager.addForum(txn, invite.getShareable()); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharerSessionState.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharerSessionState.java deleted file mode 100644 index 12280066ebc7727c918be700488f6c16c386f01a..0000000000000000000000000000000000000000 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharerSessionState.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.briarproject.briar.sharing; - -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.data.BdfDictionary; -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 javax.annotation.Nullable; -import javax.annotation.concurrent.NotThreadSafe; - -import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME; -import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT; - -@NotThreadSafe -@NotNullByDefault -class ForumSharerSessionState extends SharerSessionState { - - private final String forumName; - private final byte[] forumSalt; - - ForumSharerSessionState(SessionId sessionId, MessageId storageId, - GroupId groupId, State state, ContactId contactId, GroupId forumId, - String forumName, byte[] forumSalt, - @Nullable MessageId responseId) { - super(sessionId, storageId, groupId, state, contactId, forumId, - responseId); - - this.forumName = forumName; - this.forumSalt = forumSalt; - } - - @Override - public BdfDictionary toBdfDictionary() { - BdfDictionary d = super.toBdfDictionary(); - d.put(FORUM_NAME, getForumName()); - d.put(FORUM_SALT, getForumSalt()); - return d; - } - - String getForumName() { - return forumName; - } - - byte[] getForumSalt() { - return forumSalt; - } -} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java index c845f2c5f2e448b38062a5104890787e450466b5..a2e8d18d9742ab3b5db1d81b05005a8c90a7b943 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java @@ -1,78 +1,35 @@ package org.briarproject.briar.sharing; -import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.client.ContactGroupFactory; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.data.MetadataEncoder; import org.briarproject.bramble.api.data.MetadataParser; 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.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.ClientId; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.briar.api.client.MessageQueueManager; import org.briarproject.briar.api.client.MessageTracker; -import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.forum.Forum; -import org.briarproject.briar.api.forum.ForumFactory; -import org.briarproject.briar.api.forum.ForumInvitationRequest; -import org.briarproject.briar.api.forum.ForumInvitationResponse; -import org.briarproject.briar.api.forum.ForumManager; import org.briarproject.briar.api.forum.ForumManager.RemoveForumHook; import org.briarproject.briar.api.forum.ForumSharingManager; -import org.briarproject.briar.api.forum.ForumSharingMessage.ForumInvitation; -import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent; -import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent; -import org.briarproject.briar.api.sharing.InvitationMessage; -import java.security.SecureRandom; - -import javax.annotation.Nullable; import javax.inject.Inject; -import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME; -import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT; -import static org.briarproject.briar.api.sharing.SharingConstants.INVITATION_ID; -import static org.briarproject.briar.api.sharing.SharingConstants.RESPONSE_ID; - @NotNullByDefault -class ForumSharingManagerImpl extends - SharingManagerImpl<Forum, ForumInvitation, ForumInviteeSessionState, ForumSharerSessionState, ForumInvitationRequestReceivedEvent, ForumInvitationResponseReceivedEvent> +class ForumSharingManagerImpl extends SharingManagerImpl<Forum> implements ForumSharingManager, RemoveForumHook { - private final SFactory sFactory; - private final IFactory iFactory; - private final ISFactory isFactory; - private final SSFactory ssFactory; - private final IRFactory irFactory; - private final IRRFactory irrFactory; - @Inject - ForumSharingManagerImpl(ClientHelper clientHelper, - Clock clock, DatabaseComponent db, - ForumFactory forumFactory, - ForumManager forumManager, - MessageQueueManager messageQueueManager, - MetadataEncoder metadataEncoder, - MetadataParser metadataParser, + ForumSharingManagerImpl(DatabaseComponent db, ClientHelper clientHelper, + MetadataParser metadataParser, MessageParser<Forum> messageParser, + SessionEncoder sessionEncoder, SessionParser sessionParser, + MessageTracker messageTracker, ContactGroupFactory contactGroupFactory, - SecureRandom random, MessageTracker messageTracker) { - super(db, messageQueueManager, clientHelper, metadataParser, - metadataEncoder, random, contactGroupFactory, messageTracker, - clock); - - sFactory = new SFactory(forumFactory, forumManager); - iFactory = new IFactory(); - isFactory = new ISFactory(); - ssFactory = new SSFactory(); - irFactory = new IRFactory(sFactory); - irrFactory = new IRRFactory(); + ProtocolEngine<Forum> engine, + InvitationFactory<Forum> invitationFactory) { + super(db, clientHelper, metadataParser, messageParser, sessionEncoder, + sessionParser, messageTracker, contactGroupFactory, engine, + invitationFactory); } @Override @@ -80,221 +37,9 @@ class ForumSharingManagerImpl extends return CLIENT_ID; } - @Override - protected InvitationMessage createInvitationRequest(MessageId id, - ForumInvitation msg, ContactId contactId, GroupId forumId, - boolean available, boolean canBeOpened, long time, - boolean local, boolean sent, boolean seen, boolean read) { - return new ForumInvitationRequest(id, msg.getSessionId(), - msg.getGroupId(), contactId, forumId, msg.getForumName(), - msg.getMessage(), available, canBeOpened, time, local, sent, - seen, read); - } - - @Override - protected InvitationMessage createInvitationResponse(MessageId id, - SessionId sessionId, GroupId groupId, ContactId contactId, - GroupId forumId, boolean accept, long time, boolean local, - boolean sent, boolean seen, boolean read) { - return new ForumInvitationResponse(id, sessionId, groupId, contactId, - forumId, accept, time, local, sent, seen, read); - } - - @Override - protected ShareableFactory<Forum, ForumInvitation, ForumInviteeSessionState, ForumSharerSessionState> getSFactory() { - return sFactory; - } - - @Override - protected InvitationFactory<ForumInvitation, ForumSharerSessionState> getIFactory() { - return iFactory; - } - - @Override - protected InviteeSessionStateFactory<Forum, ForumInviteeSessionState> getISFactory() { - return isFactory; - } - - @Override - protected SharerSessionStateFactory<Forum, ForumSharerSessionState> getSSFactory() { - return ssFactory; - } - - @Override - protected InvitationReceivedEventFactory<ForumInviteeSessionState, ForumInvitationRequestReceivedEvent> getIRFactory() { - return irFactory; - } - - @Override - protected InvitationResponseReceivedEventFactory<ForumSharerSessionState, ForumInvitationResponseReceivedEvent> getIRRFactory() { - return irrFactory; - } - @Override public void removingForum(Transaction txn, Forum f) throws DbException { removingShareable(txn, f); } - private static class SFactory implements - ShareableFactory<Forum, ForumInvitation, ForumInviteeSessionState, ForumSharerSessionState> { - - private final ForumFactory forumFactory; - private final ForumManager forumManager; - - private SFactory(ForumFactory forumFactory, ForumManager forumManager) { - this.forumFactory = forumFactory; - this.forumManager = forumManager; - } - - @Override - public BdfList encode(Forum f) { - return BdfList.of(f.getName(), f.getSalt()); - } - - @Override - public Forum get(Transaction txn, GroupId groupId) - throws DbException { - return forumManager.getForum(txn, groupId); - } - - @Override - public Forum parse(BdfList shareable) throws FormatException { - return forumFactory - .createForum(shareable.getString(0), shareable.getRaw(1)); - } - - @Override - public Forum parse(ForumInvitation msg) { - return forumFactory - .createForum(msg.getForumName(), msg.getForumSalt()); - } - - @Override - public Forum parse(ForumInviteeSessionState state) { - return forumFactory - .createForum(state.getForumName(), state.getForumSalt()); - } - - @Override - public Forum parse(ForumSharerSessionState state) { - return forumFactory - .createForum(state.getForumName(), state.getForumSalt()); - } - } - - private static class IFactory implements - InvitationFactory<ForumInvitation, ForumSharerSessionState> { - @Override - public ForumInvitation build(GroupId groupId, BdfDictionary d) - throws FormatException { - return ForumInvitation.from(groupId, d); - } - - @Override - public ForumInvitation build(ForumSharerSessionState localState, - long time) { - return new ForumInvitation(localState.getContactGroupId(), - localState.getSessionId(), localState.getForumName(), - localState.getForumSalt(), time, localState.getMessage()); - } - } - - private static class ISFactory implements - InviteeSessionStateFactory<Forum, ForumInviteeSessionState> { - @Override - public ForumInviteeSessionState build(SessionId sessionId, - MessageId storageId, GroupId groupId, - InviteeSessionState.State state, ContactId contactId, - GroupId forumId, BdfDictionary d) throws FormatException { - String forumName = d.getString(FORUM_NAME); - byte[] forumSalt = d.getRaw(FORUM_SALT); - MessageId invitationId = new MessageId(d.getRaw(INVITATION_ID)); - return new ForumInviteeSessionState(sessionId, storageId, - groupId, state, contactId, forumId, forumName, forumSalt, - invitationId); - } - - @Override - public ForumInviteeSessionState build(SessionId sessionId, - MessageId storageId, GroupId groupId, - InviteeSessionState.State state, ContactId contactId, - Forum forum, MessageId invitationId) { - return new ForumInviteeSessionState(sessionId, storageId, - groupId, state, contactId, forum.getId(), forum.getName(), - forum.getSalt(), invitationId); - } - } - - private static class SSFactory implements - SharerSessionStateFactory<Forum, ForumSharerSessionState> { - @Override - public ForumSharerSessionState build(SessionId sessionId, - MessageId storageId, GroupId groupId, - SharerSessionState.State state, ContactId contactId, - GroupId forumId, BdfDictionary d) throws FormatException { - String forumName = d.getString(FORUM_NAME); - byte[] forumSalt = d.getRaw(FORUM_SALT); - MessageId responseId = null; - byte[] responseIdBytes = d.getOptionalRaw(RESPONSE_ID); - if (responseIdBytes != null) - responseId = new MessageId(responseIdBytes); - return new ForumSharerSessionState(sessionId, storageId, - groupId, state, contactId, forumId, forumName, forumSalt, - responseId); - } - - @Override - public ForumSharerSessionState build(SessionId sessionId, - MessageId storageId, GroupId groupId, - SharerSessionState.State state, ContactId contactId, - Forum forum) { - return new ForumSharerSessionState(sessionId, storageId, - groupId, state, contactId, forum.getId(), forum.getName(), - forum.getSalt(), null); - } - } - - private static class IRFactory implements - InvitationReceivedEventFactory<ForumInviteeSessionState, ForumInvitationRequestReceivedEvent> { - - private final SFactory sFactory; - - private IRFactory(SFactory sFactory) { - this.sFactory = sFactory; - } - - @Override - public ForumInvitationRequestReceivedEvent build( - ForumInviteeSessionState localState, long time, - @Nullable String msg) { - Forum forum = sFactory.parse(localState); - ContactId contactId = localState.getContactId(); - ForumInvitationRequest request = new ForumInvitationRequest( - localState.getInvitationId(), localState.getSessionId(), - localState.getContactGroupId(), contactId, - localState.getShareableId(), forum.getName(), msg, true, - false, time, false, false, false, false); - return new ForumInvitationRequestReceivedEvent(forum, contactId, - request); - } - } - - private static class IRRFactory implements - InvitationResponseReceivedEventFactory<ForumSharerSessionState, ForumInvitationResponseReceivedEvent> { - @Override - public ForumInvitationResponseReceivedEvent build( - ForumSharerSessionState localState, boolean accept, long time) { - String name = localState.getForumName(); - ContactId c = localState.getContactId(); - MessageId responseId = localState.getResponseId(); - if (responseId == null) - throw new IllegalStateException("No responseId"); - ForumInvitationResponse response = new ForumInvitationResponse( - responseId, localState.getSessionId(), - localState.getContactGroupId(), localState.getContactId(), - localState.getShareableId(), accept, time, false, false, - false, false); - return new ForumInvitationResponseReceivedEvent(name, c, response); - } - } } diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java index 7be98da26b1c6e09245272af845eeaee37bf00a3..14853dfb186df358fa232b8958aca2ebe3c25639 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java @@ -1,89 +1,47 @@ package org.briarproject.briar.sharing; import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.BdfMessageContext; import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.MetadataEncoder; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.system.Clock; -import org.briarproject.briar.api.client.SessionId; -import org.briarproject.briar.client.BdfQueueMessageValidator; +import org.briarproject.briar.api.forum.Forum; +import org.briarproject.briar.api.forum.ForumFactory; import javax.annotation.concurrent.Immutable; import javax.inject.Inject; import static org.briarproject.bramble.util.ValidationUtils.checkLength; import static org.briarproject.bramble.util.ValidationUtils.checkSize; -import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME; -import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT; import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT_LENGTH; import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH; -import static org.briarproject.briar.api.sharing.SharingConstants.INVITATION_MSG; -import static org.briarproject.briar.api.sharing.SharingConstants.LOCAL; -import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH; -import static org.briarproject.briar.api.sharing.SharingConstants.SESSION_ID; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE; -import static org.briarproject.briar.api.sharing.SharingConstants.TIME; -import static org.briarproject.briar.api.sharing.SharingConstants.TYPE; @Immutable @NotNullByDefault -class ForumSharingValidator extends BdfQueueMessageValidator { +class ForumSharingValidator extends SharingValidator { + + private final ForumFactory forumFactory; @Inject - ForumSharingValidator(ClientHelper clientHelper, - MetadataEncoder metadataEncoder, Clock clock) { - super(clientHelper, metadataEncoder, clock); + ForumSharingValidator(MessageEncoder messageEncoder, + ClientHelper clientHelper, MetadataEncoder metadataEncoder, + Clock clock, ForumFactory forumFactory) { + super(messageEncoder, clientHelper, metadataEncoder, clock); + this.forumFactory = forumFactory; } @Override - protected BdfMessageContext validateMessage(Message m, Group g, - BdfList body) throws FormatException { - - BdfDictionary d = new BdfDictionary(); - long type = body.getLong(0); - byte[] id = body.getRaw(1); - checkLength(id, SessionId.LENGTH); - - if (type == SHARE_MSG_TYPE_INVITATION) { - checkSize(body, 4, 5); - - String name = body.getString(2); - checkLength(name, 1, MAX_FORUM_NAME_LENGTH); - - byte[] salt = body.getRaw(3); - checkLength(salt, FORUM_SALT_LENGTH); - - d.put(FORUM_NAME, name); - d.put(FORUM_SALT, salt); - - if (body.size() > 4) { - String msg = body.getString(4); - checkLength(msg, 0, MAX_INVITATION_MESSAGE_LENGTH); - d.put(INVITATION_MSG, msg); - } - } else { - checkSize(body, 2); - if (type != SHARE_MSG_TYPE_ACCEPT && - type != SHARE_MSG_TYPE_DECLINE && - type != SHARE_MSG_TYPE_LEAVE && - type != SHARE_MSG_TYPE_ABORT) { - throw new FormatException(); - } - } - // Return the metadata - d.put(TYPE, type); - d.put(SESSION_ID, id); - d.put(LOCAL, false); - d.put(TIME, m.getTimestamp()); - return new BdfMessageContext(d); + protected GroupId validateDescriptor(BdfList descriptor) + throws FormatException { + checkSize(descriptor, 2); + String name = descriptor.getString(0); + checkLength(name, 1, MAX_FORUM_NAME_LENGTH); + byte[] salt = descriptor.getRaw(1); + checkLength(salt, FORUM_SALT_LENGTH); + Forum forum = forumFactory.createForum(name, salt); + return forum.getId(); } + } diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationFactory.java index 4b55d3c6b94b42647b006b1d62e17011d61de4b5..ab9fa0e23df0ae810227d9352830cadcd5348b20 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationFactory.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationFactory.java @@ -1,11 +1,21 @@ package org.briarproject.briar.sharing; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.briar.api.sharing.SharingMessage; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.sharing.InvitationRequest; +import org.briarproject.briar.api.sharing.InvitationResponse; +import org.briarproject.briar.api.sharing.Shareable; -@NotNullByDefault -interface InvitationFactory<I extends SharingMessage.Invitation, SS extends SharerSessionState> - extends org.briarproject.briar.api.sharing.InvitationFactory<I> { +public interface InvitationFactory<S extends Shareable> { + + InvitationRequest<S> createInvitationRequest(boolean local, boolean sent, + boolean seen, boolean read, InviteMessage<S> m, ContactId c, + boolean available, boolean canBeOpened); + + InvitationResponse createInvitationResponse(MessageId id, + GroupId contactGroupId, long time, boolean local, boolean sent, + boolean seen, boolean read, GroupId shareableId, + ContactId contactId, boolean accept); - I build(SS localState, long time); } diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java index 9c1e3f4e3c3196227800fd3a0b18d36edcdb3ffe..07d81557e680b4aaa640e1cf28a73f30f967e713 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java @@ -5,6 +5,7 @@ import org.briarproject.briar.api.sharing.event.InvitationRequestReceivedEvent; import javax.annotation.Nullable; +@Deprecated @NotNullByDefault interface InvitationReceivedEventFactory<IS extends InviteeSessionState, IR extends InvitationRequestReceivedEvent> { diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationResponseReceivedEventFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationResponseReceivedEventFactory.java index 31ca766b2620173a4d75f593a5ec7b973971e436..2a36e36aa5547048b86988a93b85141d3e323bc2 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationResponseReceivedEventFactory.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationResponseReceivedEventFactory.java @@ -3,6 +3,7 @@ package org.briarproject.briar.sharing; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent; +@Deprecated @NotNullByDefault interface InvitationResponseReceivedEventFactory<SS extends SharerSessionState, IRR extends InvitationResponseReceivedEvent> { diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..f6343cd8db2cb4656af61208fdc248cf3bd16d7b --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteMessage.java @@ -0,0 +1,39 @@ +package org.briarproject.briar.sharing; + +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.sharing.Shareable; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class InviteMessage<S extends Shareable> extends SharingMessage { + + private final S shareable; + @Nullable + private final String message; + + InviteMessage(MessageId id, @Nullable MessageId previousMessageId, + GroupId contactGroupId, S shareable, @Nullable String message, + long timestamp) { + super(id, contactGroupId, shareable.getId(), timestamp, + previousMessageId); + if (message != null && message.equals("")) + throw new IllegalArgumentException(); + this.shareable = shareable; + this.message = message; + } + + public S getShareable() { + return shareable; + } + + @Nullable + public String getMessage() { + return message; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeEngine.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeEngine.java index 92a534da64ccaf1e6208023b9d0157d58319200f..bdb0be196dc2589beabf6ad2a3650a4a64f07b64 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeEngine.java @@ -28,6 +28,7 @@ import static org.briarproject.briar.api.sharing.SharingConstants.TASK_UNSHARE_S import static org.briarproject.briar.api.sharing.SharingMessage.BaseMessage; import static org.briarproject.briar.api.sharing.SharingMessage.SimpleMessage; +@Deprecated @Immutable @NotNullByDefault class InviteeEngine<IS extends InviteeSessionState, IR extends InvitationRequestReceivedEvent> diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionState.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionState.java index cb90748f701c6122243037e9f6fbcec1f4f05d88..efd35e5be85b6adb26ffabb864b9f7d7d333ed40 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionState.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionState.java @@ -23,6 +23,7 @@ import static org.briarproject.briar.sharing.InviteeSessionState.Action.LOCAL_LE import static org.briarproject.briar.sharing.InviteeSessionState.Action.REMOTE_INVITATION; import static org.briarproject.briar.sharing.InviteeSessionState.Action.REMOTE_LEAVE; +@Deprecated @NotThreadSafe @NotNullByDefault public abstract class InviteeSessionState extends SharingSessionState { diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionStateFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionStateFactory.java index 3a0e3813e9810c1663e21c7a0dddbba0dd7ebe5f..5c827921e883a94ce0123ac08b0b8c2ec66fea79 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionStateFactory.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InviteeSessionStateFactory.java @@ -8,6 +8,7 @@ import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.sharing.Shareable; +@Deprecated interface InviteeSessionStateFactory<S extends Shareable, IS extends InviteeSessionState> { IS build(SessionId sessionId, MessageId storageId, GroupId groupId, diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/LeaveMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/LeaveMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..f518570551a286d843616f0792a76f961f03a843 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/LeaveMessage.java @@ -0,0 +1,20 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class LeaveMessage extends SharingMessage { + + LeaveMessage(MessageId id, GroupId contactGroupId, + GroupId shareableId, long timestamp, + @Nullable MessageId previousMessageId) { + super(id, contactGroupId, shareableId, timestamp, previousMessageId); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoder.java new file mode 100644 index 0000000000000000000000000000000000000000..33ce9b3fc1b160398913f7b205c89080dad838a3 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoder.java @@ -0,0 +1,39 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.Nullable; + +@NotNullByDefault +interface MessageEncoder { + + BdfDictionary encodeMetadata(MessageType type, GroupId shareableId, + long timestamp, boolean local, boolean read, boolean visible, + boolean available); + + void setVisibleInUi(BdfDictionary meta, boolean visible); + + void setAvailableToAnswer(BdfDictionary meta, boolean available); + + Message encodeInviteMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, BdfList descriptor, + @Nullable String message); + + Message encodeAcceptMessage(GroupId contactGroupId, GroupId shareableId, + long timestamp, @Nullable MessageId previousMessageId); + + Message encodeDeclineMessage(GroupId contactGroupId, GroupId shareableId, + long timestamp, @Nullable MessageId previousMessageId); + + Message encodeLeaveMessage(GroupId contactGroupId, GroupId shareableId, + long timestamp, @Nullable MessageId previousMessageId); + + Message encodeAbortMessage(GroupId contactGroupId, GroupId shareableId, + long timestamp, @Nullable MessageId previousMessageId); + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoderImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..1047df487e05596c61a603bc6973e759a2842809 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoderImpl.java @@ -0,0 +1,137 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageFactory; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +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.SharingConstants.MSG_KEY_AVAILABLE_TO_ANSWER; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_LOCAL; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_MESSAGE_TYPE; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_SHAREABLE_ID; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_READ; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_TIMESTAMP; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_VISIBLE_IN_UI; + +@Immutable +@NotNullByDefault +class MessageEncoderImpl implements MessageEncoder { + + private final ClientHelper clientHelper; + private final MessageFactory messageFactory; + + @Inject + MessageEncoderImpl(ClientHelper clientHelper, + MessageFactory messageFactory) { + this.clientHelper = clientHelper; + this.messageFactory = messageFactory; + } + + @Override + public BdfDictionary encodeMetadata(MessageType type, + GroupId shareableId, long timestamp, boolean local, boolean read, + boolean visible, boolean available) { + BdfDictionary meta = new BdfDictionary(); + meta.put(MSG_KEY_MESSAGE_TYPE, type.getValue()); + meta.put(MSG_KEY_SHAREABLE_ID, shareableId); + meta.put(MSG_KEY_TIMESTAMP, timestamp); + meta.put(MSG_KEY_LOCAL, local); + meta.put(MSG_KEY_READ, read); + meta.put(MSG_KEY_VISIBLE_IN_UI, visible); + meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available); + return meta; + } + + @Override + public void setVisibleInUi(BdfDictionary meta, boolean visible) { + meta.put(MSG_KEY_VISIBLE_IN_UI, visible); + } + + @Override + public void setAvailableToAnswer(BdfDictionary meta, boolean available) { + meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available); + } + + @Override + public Message encodeInviteMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, BdfList descriptor, + @Nullable String message) { + if (message != null && message.equals("")) + throw new IllegalArgumentException(); + BdfList body = BdfList.of( + INVITE.getValue(), + previousMessageId, + descriptor, + message + ); + try { + return messageFactory.createMessage(contactGroupId, timestamp, + clientHelper.toByteArray(body)); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + @Override + public Message encodeAcceptMessage(GroupId contactGroupId, + GroupId shareableId, long timestamp, + @Nullable MessageId previousMessageId) { + return encodeMessage(ACCEPT, contactGroupId, shareableId, timestamp, + previousMessageId); + } + + @Override + public Message encodeDeclineMessage(GroupId contactGroupId, + GroupId shareableId, long timestamp, + @Nullable MessageId previousMessageId) { + return encodeMessage(DECLINE, contactGroupId, shareableId, timestamp, + previousMessageId); + } + + @Override + public Message encodeLeaveMessage(GroupId contactGroupId, + GroupId shareableId, long timestamp, + @Nullable MessageId previousMessageId) { + return encodeMessage(LEAVE, contactGroupId, shareableId, timestamp, + previousMessageId); + } + + @Override + public Message encodeAbortMessage(GroupId contactGroupId, + GroupId shareableId, long timestamp, + @Nullable MessageId previousMessageId) { + return encodeMessage(ABORT, contactGroupId, shareableId, timestamp, + previousMessageId); + } + + private Message encodeMessage(MessageType type, GroupId contactGroupId, + GroupId shareableId, long timestamp, + @Nullable MessageId previousMessageId) { + BdfList body = BdfList.of( + type.getValue(), + shareableId, + previousMessageId + ); + try { + return messageFactory.createMessage(contactGroupId, timestamp, + clientHelper.toByteArray(body)); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageMetadata.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageMetadata.java new file mode 100644 index 0000000000000000000000000000000000000000..031676249a047b696efd1ec47247637d092dec7f --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageMetadata.java @@ -0,0 +1,56 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class MessageMetadata { + + private final MessageType type; + private final GroupId shareableId; + private final long timestamp; + private final boolean local, read, visible, available; + + MessageMetadata(MessageType type, GroupId shareableId, long timestamp, + boolean local, boolean read, boolean visible, boolean available) { + this.shareableId = shareableId; + this.type = type; + this.timestamp = timestamp; + this.local = local; + this.read = read; + this.visible = visible; + this.available = available; + } + + MessageType getMessageType() { + return type; + } + + GroupId getShareableId() { + return shareableId; + } + + long getTimestamp() { + return timestamp; + } + + boolean isLocal() { + return local; + } + + boolean isRead() { + return read; + } + + boolean isVisibleInConversation() { + return visible; + } + + boolean isAvailableToAnswer() { + return available; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParser.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParser.java new file mode 100644 index 0000000000000000000000000000000000000000..869d017ad1a2c0427a54b5d257ad1a9815fd96bc --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParser.java @@ -0,0 +1,43 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.sharing.Shareable; + +@NotNullByDefault +interface MessageParser<S extends Shareable> { + + BdfDictionary getMessagesVisibleInUiQuery(); + + BdfDictionary getInvitesAvailableToAnswerQuery(); + + BdfDictionary getInvitesAvailableToAnswerQuery(GroupId shareableId); + + MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException; + + InviteMessage<S> getInviteMessage(Transaction txn, MessageId m) + throws DbException, FormatException; + + InviteMessage<S> parseInviteMessage(Message m, BdfList body) + throws FormatException; + + AcceptMessage parseAcceptMessage(Message m, BdfList body) + throws FormatException; + + DeclineMessage parseDeclineMessage(Message m, BdfList body) + throws FormatException; + + LeaveMessage parseLeaveMessage(Message m, BdfList body) + throws FormatException; + + AbortMessage parseAbortMessage(Message m, BdfList body) + throws FormatException; + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParserImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4384941179563df52a39de121ef693ea38e581d8 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageParserImpl.java @@ -0,0 +1,139 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.sharing.Shareable; + +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.briar.sharing.MessageType.INVITE; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_AVAILABLE_TO_ANSWER; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_LOCAL; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_MESSAGE_TYPE; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_READ; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_SHAREABLE_ID; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_TIMESTAMP; +import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_VISIBLE_IN_UI; + +@Immutable +@NotNullByDefault +abstract class MessageParserImpl<S extends Shareable> + implements MessageParser<S> { + + private final ClientHelper clientHelper; + + MessageParserImpl(ClientHelper clientHelper) { + this.clientHelper = clientHelper; + } + + @Override + public BdfDictionary getMessagesVisibleInUiQuery() { + return BdfDictionary.of(new BdfEntry(MSG_KEY_VISIBLE_IN_UI, true)); + } + + @Override + public BdfDictionary getInvitesAvailableToAnswerQuery() { + return BdfDictionary.of( + new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true), + new BdfEntry(MSG_KEY_MESSAGE_TYPE, INVITE.getValue()) + ); + } + + @Override + public BdfDictionary getInvitesAvailableToAnswerQuery(GroupId shareableId) { + return BdfDictionary.of( + new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true), + new BdfEntry(MSG_KEY_MESSAGE_TYPE, INVITE.getValue()), + new BdfEntry(MSG_KEY_SHAREABLE_ID, shareableId) + ); + } + + @Override + public MessageMetadata parseMetadata(BdfDictionary meta) + throws FormatException { + MessageType type = MessageType + .fromValue(meta.getLong(MSG_KEY_MESSAGE_TYPE).intValue()); + GroupId shareableId = new GroupId(meta.getRaw(MSG_KEY_SHAREABLE_ID)); + long timestamp = meta.getLong(MSG_KEY_TIMESTAMP); + boolean local = meta.getBoolean(MSG_KEY_LOCAL); + boolean read = meta.getBoolean(MSG_KEY_READ, false); + boolean visible = meta.getBoolean(MSG_KEY_VISIBLE_IN_UI, false); + boolean available = meta.getBoolean(MSG_KEY_AVAILABLE_TO_ANSWER, false); + return new MessageMetadata(type, shareableId, timestamp, local, read, + visible, available); + } + + @Override + public InviteMessage<S> getInviteMessage(Transaction txn, MessageId m) + throws DbException, FormatException { + Message message = clientHelper.getMessage(txn, m); + if (message == null) throw new DbException(); + BdfList body = clientHelper.toList(message); + return parseInviteMessage(message, body); + } + + @Override + public InviteMessage<S> parseInviteMessage(Message m, BdfList body) + throws FormatException { + byte[] b = body.getOptionalRaw(1); + MessageId previousMessageId = (b == null ? null : new MessageId(b)); + BdfList descriptor = body.getList(2); + S shareable = createShareable(descriptor); + String message = body.getOptionalString(3); + return new InviteMessage<S>(m.getId(), previousMessageId, + m.getGroupId(), shareable, message, m.getTimestamp()); + } + + protected abstract S createShareable(BdfList descriptor) + throws FormatException; + + @Override + public AcceptMessage parseAcceptMessage(Message m, BdfList body) + throws FormatException { + GroupId shareableId = new GroupId(body.getRaw(1)); + byte[] b = body.getOptionalRaw(2); + MessageId previousMessageId = (b == null ? null : new MessageId(b)); + return new AcceptMessage(m.getId(), previousMessageId, m.getGroupId(), + shareableId, m.getTimestamp()); + } + + @Override + public DeclineMessage parseDeclineMessage(Message m, BdfList body) + throws FormatException { + GroupId shareableId = new GroupId(body.getRaw(1)); + byte[] b = body.getOptionalRaw(2); + MessageId previousMessageId = (b == null ? null : new MessageId(b)); + return new DeclineMessage(m.getId(), m.getGroupId(), shareableId, + m.getTimestamp(), previousMessageId); + } + + @Override + public LeaveMessage parseLeaveMessage(Message m, BdfList body) + throws FormatException { + GroupId shareableId = new GroupId(body.getRaw(1)); + byte[] b = body.getOptionalRaw(2); + MessageId previousMessageId = (b == null ? null : new MessageId(b)); + return new LeaveMessage(m.getId(), m.getGroupId(), shareableId, + m.getTimestamp(), previousMessageId); + } + + @Override + public AbortMessage parseAbortMessage(Message m, BdfList body) + throws FormatException { + GroupId shareableId = new GroupId(body.getRaw(1)); + byte[] b = body.getOptionalRaw(2); + MessageId previousMessageId = (b == null ? null : new MessageId(b)); + return new AbortMessage(m.getId(), m.getGroupId(), shareableId, + m.getTimestamp(), previousMessageId); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageType.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageType.java new file mode 100644 index 0000000000000000000000000000000000000000..1af500a745d73bd0178c44551bc8f88a7fef6e65 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageType.java @@ -0,0 +1,29 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +enum MessageType { + + INVITE(0), ACCEPT(1), DECLINE(2), LEAVE(3), ABORT(4); + + private final int value; + + MessageType(int value) { + this.value = value; + } + + int getValue() { + return value; + } + + static MessageType fromValue(int value) throws FormatException { + for (MessageType m : values()) if (m.value == value) return m; + throw new FormatException(); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/OldInvitationFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/OldInvitationFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..7f127a1c826251c03914e9479d10a32b74edd6b3 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/OldInvitationFactory.java @@ -0,0 +1,12 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.api.sharing.SharingMessage; + +@Deprecated +@NotNullByDefault +interface OldInvitationFactory<I extends SharingMessage.Invitation, SS extends SharerSessionState> + extends org.briarproject.briar.api.sharing.InvitationFactory<I> { + + I build(SS localState, long time); +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/OldSharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/OldSharingManagerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..613ee0bed8aa2570a15b45ccab6c579a4df3fa5d --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/OldSharingManagerImpl.java @@ -0,0 +1,1052 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.Bytes; +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.contact.ContactManager.AddContactHook; +import org.briarproject.bramble.api.contact.ContactManager.RemoveContactHook; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.data.MetadataEncoder; +import org.briarproject.bramble.api.data.MetadataParser; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Metadata; +import org.briarproject.bramble.api.db.NoSuchMessageException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.Client; +import org.briarproject.bramble.api.sync.ClientId; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.sync.MessageStatus; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.util.StringUtils; +import org.briarproject.briar.api.client.MessageQueueManager; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.sharing.InvitationMessage; +import org.briarproject.briar.api.sharing.Shareable; +import org.briarproject.briar.api.sharing.SharingInvitationItem; +import org.briarproject.briar.api.sharing.SharingManager; +import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent; +import org.briarproject.briar.api.sharing.event.InvitationRequestReceivedEvent; +import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent; +import org.briarproject.briar.client.ConversationClientImpl; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; +import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; +import static org.briarproject.briar.api.client.ProtocolEngine.StateUpdate; +import static org.briarproject.briar.api.sharing.SharingConstants.CONTACT_ID; +import static org.briarproject.briar.api.sharing.SharingConstants.IS_SHARER; +import static org.briarproject.briar.api.sharing.SharingConstants.LOCAL; +import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH; +import static org.briarproject.briar.api.sharing.SharingConstants.SESSION_ID; +import static org.briarproject.briar.api.sharing.SharingConstants.SHAREABLE_ID; +import static org.briarproject.briar.api.sharing.SharingConstants.SHARED_BY_US; +import static org.briarproject.briar.api.sharing.SharingConstants.SHARED_WITH_US; +import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT; +import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT; +import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE; +import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION; +import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE; +import static org.briarproject.briar.api.sharing.SharingConstants.SHARING_SALT_LENGTH; +import static org.briarproject.briar.api.sharing.SharingConstants.STATE; +import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHAREABLE_TO_LIST_SHARED_WITH_US; +import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US; +import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHARED_SHAREABLE; +import static org.briarproject.briar.api.sharing.SharingConstants.TASK_REMOVE_SHAREABLE_FROM_LIST_SHARED_WITH_US; +import static org.briarproject.briar.api.sharing.SharingConstants.TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US; +import static org.briarproject.briar.api.sharing.SharingConstants.TASK_SHARE_SHAREABLE; +import static org.briarproject.briar.api.sharing.SharingConstants.TASK_UNSHARE_SHAREABLE_SHARED_BY_US; +import static org.briarproject.briar.api.sharing.SharingConstants.TASK_UNSHARE_SHAREABLE_SHARED_WITH_US; +import static org.briarproject.briar.api.sharing.SharingConstants.TIME; +import static org.briarproject.briar.api.sharing.SharingConstants.TO_BE_SHARED_BY_US; +import static org.briarproject.briar.api.sharing.SharingConstants.TYPE; +import static org.briarproject.briar.api.sharing.SharingMessage.BaseMessage; +import static org.briarproject.briar.api.sharing.SharingMessage.Invitation; +import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; +import static org.briarproject.briar.sharing.InviteeSessionState.State.AWAIT_LOCAL_RESPONSE; + +@Deprecated +@NotNullByDefault +abstract class OldSharingManagerImpl<S extends Shareable, I extends Invitation, IS extends InviteeSessionState, SS extends SharerSessionState, IR extends InvitationRequestReceivedEvent, IRR extends InvitationResponseReceivedEvent> + extends ConversationClientImpl + implements SharingManager<S>, Client, AddContactHook, + RemoveContactHook { + + private static final Logger LOG = + Logger.getLogger(OldSharingManagerImpl.class.getName()); + + private final MessageQueueManager messageQueueManager; + private final MetadataEncoder metadataEncoder; + private final SecureRandom random; + private final ContactGroupFactory contactGroupFactory; + private final Clock clock; + private final Group localGroup; + + OldSharingManagerImpl(DatabaseComponent db, + MessageQueueManager messageQueueManager, ClientHelper clientHelper, + MetadataParser metadataParser, MetadataEncoder metadataEncoder, + SecureRandom random, ContactGroupFactory contactGroupFactory, + MessageTracker messageTracker, Clock clock) { + super(db, clientHelper, metadataParser, messageTracker); + this.messageQueueManager = messageQueueManager; + this.metadataEncoder = metadataEncoder; + this.random = random; + this.contactGroupFactory = contactGroupFactory; + this.clock = clock; + localGroup = contactGroupFactory.createLocalGroup(getClientId()); + } + + protected abstract ClientId getClientId(); + + protected abstract InvitationMessage createInvitationRequest(MessageId id, + I msg, ContactId contactId, GroupId blogId, boolean available, + boolean canBeOpened, long time, boolean local, boolean sent, + boolean seen, boolean read); + + protected abstract InvitationMessage createInvitationResponse(MessageId id, + SessionId sessionId, GroupId groupId, ContactId contactId, + GroupId blogId, boolean accept, long time, boolean local, + boolean sent, boolean seen, boolean read); + + protected abstract ShareableFactory<S, I, IS, SS> getSFactory(); + + protected abstract OldInvitationFactory<I, SS> getIFactory(); + + protected abstract InviteeSessionStateFactory<S, IS> getISFactory(); + + protected abstract SharerSessionStateFactory<S, SS> getSSFactory(); + + protected abstract InvitationReceivedEventFactory<IS, IR> getIRFactory(); + + protected abstract InvitationResponseReceivedEventFactory<SS, IRR> getIRRFactory(); + + @Override + public void createLocalState(Transaction txn) throws DbException { + db.addGroup(txn, localGroup); + // Ensure we've set things up for any pre-existing contacts + for (Contact c : db.getContacts(txn)) addingContact(txn, c); + } + + @Override + public void addingContact(Transaction txn, Contact c) throws DbException { + try { + // Create a group to share with the contact + Group g = getContactGroup(c); + // Return if we've already set things up for this contact + if (db.containsGroup(txn, g.getId())) return; + // Store the group and share it with the contact + db.addGroup(txn, g); + db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED); + // Attach the contact ID to the group + BdfDictionary meta = new BdfDictionary(); + meta.put(CONTACT_ID, c.getId().getInt()); + meta.put(TO_BE_SHARED_BY_US, new BdfList()); + meta.put(SHARED_BY_US, new BdfList()); + meta.put(SHARED_WITH_US, new BdfList()); + clientHelper.mergeGroupMetadata(txn, g.getId(), meta); + } catch (FormatException e) { + throw new DbException(e); + } + } + + @Override + public void removingContact(Transaction txn, Contact c) throws DbException { + // query for this contact c + BdfDictionary query = BdfDictionary.of( + new BdfEntry(CONTACT_ID, c.getId().getInt()) + ); + + // clean up session states with that contact from localGroup + try { + Map<MessageId, BdfDictionary> map = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId(), + query); + for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) { + deleteMessage(txn, entry.getKey()); + } + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + + // remove the contact group (all messages will be removed with it) + db.removeGroup(txn, getContactGroup(c)); + } + + @Override + protected boolean incomingMessage(Transaction txn, Message m, BdfList body, + BdfDictionary d) throws DbException, FormatException { + + BaseMessage msg = BaseMessage.from(getIFactory(), m.getGroupId(), d); + SessionId sessionId = msg.getSessionId(); + + if (msg.getType() == SHARE_MSG_TYPE_INVITATION) { + // we are an invitee who just received a new invitation + boolean stateExists = true; + try { + // check if we have a session with that ID already + getSessionState(txn, sessionId, false); + } catch (FormatException e) { + // this is what we would expect under normal circumstances + stateExists = false; + } + // check if we already have a state with that sessionId + if (stateExists) throw new FormatException(); + + // check if shareable can be shared + I invitation = (I) msg; + S f = getSFactory().parse(invitation); + ContactId contactId = getContactId(txn, m.getGroupId()); + Contact contact = db.getContact(txn, contactId); + if (!canBeShared(txn, f.getId(), contact)) + checkForRaceCondition(txn, f, contact); + + // initialize state and process invitation + IS state = initializeInviteeState(txn, contactId, invitation, + m.getId()); + InviteeEngine<IS, IR> engine = + new InviteeEngine<IS, IR>(getIRFactory(), clock); + processInviteeStateUpdate(txn, m.getId(), + engine.onMessageReceived(state, msg)); + messageTracker.trackIncomingMessage(txn, m); + } else if (msg.getType() == SHARE_MSG_TYPE_ACCEPT || + msg.getType() == SHARE_MSG_TYPE_DECLINE) { + // we are a sharer who just received a response + SS state = getSessionStateForSharer(txn, sessionId); + state.setResponseId(m.getId()); + SharerEngine<I, SS, IRR> engine = + new SharerEngine<I, SS, IRR>(getIFactory(), + getIRRFactory(), clock); + processSharerStateUpdate(txn, m.getId(), + engine.onMessageReceived(state, msg)); + messageTracker.trackIncomingMessage(txn, m); + } else if (msg.getType() == SHARE_MSG_TYPE_LEAVE || + msg.getType() == SHARE_MSG_TYPE_ABORT) { + // we don't know who we are, so figure it out + SharingSessionState s = getSessionState(txn, sessionId, true); + if (s instanceof SharerSessionState) { + // we are a sharer and the invitee wants to leave or abort + SS state = (SS) s; + SharerEngine<I, SS, IRR> engine = + new SharerEngine<I, SS, IRR>(getIFactory(), + getIRRFactory(), clock); + processSharerStateUpdate(txn, m.getId(), + engine.onMessageReceived(state, msg)); + } else { + // we are an invitee and the sharer wants to leave or abort + IS state = (IS) s; + InviteeEngine<IS, IR> engine = + new InviteeEngine<IS, IR>(getIRFactory(), clock); + processInviteeStateUpdate(txn, m.getId(), + engine.onMessageReceived(state, msg)); + } + } else { + // message has passed validator, so that should never happen + throw new AssertionError("Illegal Sharing Message"); + } + // don't share message as other party already has it + return false; + } + + @Override + public void sendInvitation(GroupId groupId, ContactId contactId, + @Nullable String msg, long timestamp) throws DbException { + + Transaction txn = db.startTransaction(false); + try { + // initialize local state for sharer + S f = getSFactory().get(txn, groupId); + SS localState = initializeSharerState(txn, f, contactId); + + // add invitation message to local state to be available for engine + if (!StringUtils.isNullOrEmpty(msg)) { + int msgLength = StringUtils.toUtf8(msg).length; + if (msgLength > MAX_INVITATION_MESSAGE_LENGTH) + throw new IllegalArgumentException(); + localState.setMessage(msg); + } + + // start engine and process its state update + SharerEngine<I, SS, IRR> engine = + new SharerEngine<I, SS, IRR>(getIFactory(), + getIRRFactory(), clock); + StateUpdate<SS, BaseMessage> update = + engine.onLocalAction(localState, + SharerSessionState.Action.LOCAL_INVITATION); + processSharerStateUpdate(txn, null, update); + + // track message + // TODO handle this properly without engine hacks (#376) + long time = update.toSend.get(0).getTime(); + messageTracker.trackMessage(txn, localState.getContactGroupId(), time, + true); + + db.commitTransaction(txn); + } catch (FormatException e) { + throw new DbException(); + } finally { + db.endTransaction(txn); + } + } + + @Override + public void respondToInvitation(S f, Contact c, boolean accept) + throws DbException { + + Transaction txn = db.startTransaction(false); + try { + // find session state based on shareable + IS localState = getSessionStateForResponse(txn, f, c); + respondToInvitation(txn, localState, accept); + db.commitTransaction(txn); + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + } + + @Override + public void respondToInvitation(ContactId c, SessionId id, boolean accept) + throws DbException { + + Transaction txn = db.startTransaction(false); + try { + IS localState = (IS) getSessionState(txn, id, true); + respondToInvitation(txn, localState, accept); + db.commitTransaction(txn); + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + } + + private void respondToInvitation(Transaction txn, IS localState, + boolean accept) throws DbException, FormatException { + // define action + InviteeSessionState.Action localAction; + if (accept) { + localAction = InviteeSessionState.Action.LOCAL_ACCEPT; + } else { + localAction = InviteeSessionState.Action.LOCAL_DECLINE; + } + + // start engine and process its state update + InviteeEngine<IS, IR> engine = + new InviteeEngine<IS, IR>(getIRFactory(), clock); + StateUpdate<IS, BaseMessage> update = + engine.onLocalAction(localState, localAction); + processInviteeStateUpdate(txn, null, update); + + // track message + // TODO handle this properly without engine hacks (#376) + long time = update.toSend.get(0).getTime(); + messageTracker.trackMessage(txn, localState.getContactGroupId(), time, true); + } + + @Override + public Collection<InvitationMessage> getInvitationMessages( + ContactId contactId) + throws DbException { + + Transaction txn = db.startTransaction(true); + try { + Contact contact = db.getContact(txn, contactId); + Group group = getContactGroup(contact); + + Collection<InvitationMessage> list = + new ArrayList<InvitationMessage>(); + Map<MessageId, BdfDictionary> map = clientHelper + .getMessageMetadataAsDictionary(txn, group.getId()); + for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { + BdfDictionary d = m.getValue(); + long type = d.getLong(TYPE); + if (type == SHARE_MSG_TYPE_LEAVE || + type == SHARE_MSG_TYPE_ABORT) continue; + try { + MessageStatus status = + db.getMessageStatus(txn, contactId, m.getKey()); + SharingSessionState s; + long time = d.getLong(TIME); + boolean local = d.getBoolean(LOCAL); + boolean read = d.getBoolean(MSG_KEY_READ, false); + boolean available = false, canBeOpened = false; + + if (type == SHARE_MSG_TYPE_INVITATION) { + I msg = getIFactory().build(group.getId(), d); + SessionId sessionId = msg.getSessionId(); + s = getSessionState(txn, sessionId, true); + if (!local) { + // figure out whether the shareable is still available + if (!(s instanceof InviteeSessionState)) + continue; + available = ((InviteeSessionState) s).getState() == + AWAIT_LOCAL_RESPONSE; + if (!available) { + canBeOpened = db.containsGroup(txn, + s.getShareableId()); + } + } + InvitationMessage im = + createInvitationRequest(m.getKey(), msg, + contactId, s.getShareableId(), + available, canBeOpened, time, local, + status.isSent(), status.isSeen(), read); + list.add(im); + } else if (type == SHARE_MSG_TYPE_ACCEPT || + type == SHARE_MSG_TYPE_DECLINE) { + boolean accept = type == SHARE_MSG_TYPE_ACCEPT; + BaseMessage msg = BaseMessage + .from(getIFactory(), group.getId(), d); + SessionId sessionId = msg.getSessionId(); + s = getSessionState(txn, sessionId, true); + InvitationMessage im = + createInvitationResponse(m.getKey(), sessionId, + group.getId(), contactId, + s.getShareableId(), accept, time, local, + status.isSent(), status.isSeen(), read); + list.add(im); + } else { + throw new RuntimeException("Unexpected Message Type"); + } + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + db.commitTransaction(txn); + return list; + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + } + + @Override + public Collection<SharingInvitationItem> getInvitations() + throws DbException { + List<SharingInvitationItem> invitations = + new ArrayList<SharingInvitationItem>(); + Transaction txn = db.startTransaction(true); + try { + Set<S> shareables = new HashSet<S>(); + Map<GroupId, Collection<Contact>> newSharers = + new HashMap<GroupId, Collection<Contact>>(); + Collection<Contact> contacts = db.getContacts(txn); + + // get invitations from each contact + for (Contact contact : contacts) { + Collection<S> newShareables = getInvited(txn, contact); + shareables.addAll(newShareables); + for (S s : newShareables) { + if (newSharers.containsKey(s.getId())) { + newSharers.get(s.getId()).add(contact); + } else { + Collection<Contact> c = new ArrayList<Contact>(); + c.add(contact); + newSharers.put(s.getId(), c); + } + } + } + // construct InvitationItem objects + for (S s : shareables) { + Collection<Contact> newS = newSharers.get(s.getId()); + boolean subscribed = db.containsGroup(txn, s.getId()); + SharingInvitationItem invitation = + new SharingInvitationItem(s, subscribed, newS); + invitations.add(invitation); + } + db.commitTransaction(txn); + return invitations; + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + } + + private Collection<S> getInvited(Transaction txn, Contact contact) + throws DbException, FormatException { + + // query for all external invitations + BdfDictionary query = BdfDictionary.of( + new BdfEntry(TYPE, SHARE_MSG_TYPE_INVITATION), + new BdfEntry(LOCAL, false) + ); + Group group = getContactGroup(contact); + + Set<S> invited = new HashSet<S>(); + Map<MessageId, BdfDictionary> map = clientHelper + .getMessageMetadataAsDictionary(txn, group.getId(), query); + for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { + BdfDictionary d = m.getValue(); + try { + I msg = getIFactory().build(group.getId(), d); + IS iss = (IS) getSessionState(txn, msg.getSessionId(), true); + // get and add the shareable if the invitation is unanswered + if (iss.getState().equals(AWAIT_LOCAL_RESPONSE)) { + S s = getSFactory().parse(iss); + invited.add(s); + } + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + return invited; + } + + @Override + public Collection<Contact> getSharedWith(GroupId g) throws DbException { + try { + List<Contact> shared = new ArrayList<Contact>(); + Transaction txn = db.startTransaction(true); + try { + for (Contact c : db.getContacts(txn)) { + GroupId contactGroup = getContactGroup(c).getId(); + if (listContains(txn, contactGroup, g, SHARED_BY_US)) + shared.add(c); + else if (listContains(txn, contactGroup, g, SHARED_WITH_US)) + shared.add(c); + } + db.commitTransaction(txn); + } finally { + db.endTransaction(txn); + } + return shared; + } catch (FormatException e) { + throw new DbException(e); + } + } + + @Override + public boolean canBeShared(GroupId g, Contact c) throws DbException { + boolean canBeShared; + Transaction txn = db.startTransaction(true); + try { + canBeShared = canBeShared(txn, g, c); + db.commitTransaction(txn); + } finally { + db.endTransaction(txn); + } + return canBeShared; + } + + protected boolean canBeShared(Transaction txn, GroupId g, Contact c) + throws DbException { + + try { + GroupId contactGroup = getContactGroup(c).getId(); + return !listContains(txn, contactGroup, g, SHARED_BY_US) && + !listContains(txn, contactGroup, g, SHARED_WITH_US) && + !listContains(txn, contactGroup, g, TO_BE_SHARED_BY_US); + } catch (FormatException e) { + throw new DbException(e); + } + } + + void removingShareable(Transaction txn, S f) throws DbException { + try { + for (Contact c : db.getContacts(txn)) { + GroupId g = getContactGroup(c).getId(); + if (removeFromList(txn, g, TO_BE_SHARED_BY_US, f)) { + leaveShareable(txn, c.getId(), f); + } + if (removeFromList(txn, g, SHARED_BY_US, f)) { + leaveShareable(txn, c.getId(), f); + } + if (removeFromList(txn, g, SHARED_WITH_US, f)) { + leaveShareable(txn, c.getId(), f); + } + } + } catch (IOException e) { + throw new DbException(e); + } + } + + private void checkForRaceCondition(Transaction txn, S f, Contact c) + throws FormatException, DbException { + + GroupId contactGroup = getContactGroup(c).getId(); + if (!listContains(txn, contactGroup, f.getId(), TO_BE_SHARED_BY_US)) + // no race-condition, this invitation is invalid + throw new FormatException(); + + // we have an invitation race condition + LocalAuthor author = db.getLocalAuthor(txn, c.getLocalAuthorId()); + Bytes ourKey = new Bytes(author.getPublicKey()); + Bytes theirKey = new Bytes(c.getAuthor().getPublicKey()); + + // determine which invitation takes precedence + boolean alice = ourKey.compareTo(theirKey) < 0; + + if (alice) { + // our own invitation takes precedence, so just delete Bob's + LOG.info( + "Invitation race-condition: We are Alice deleting Bob's invitation."); + throw new FormatException(); + } else { + // we are Bob, so we need to "take back" our own invitation + LOG.info( + "Invitation race-condition: We are Bob taking back our invitation."); + SharingSessionState state = + getSessionStateForLeaving(txn, f, c.getId()); + if (state instanceof SharerSessionState) { + //SharerEngine engine = new SharerEngine(); + //processSharerStateUpdate(txn, null, + // engine.onLocalAction((SharerSessionState) state, + // Action.LOCAL_LEAVE)); + + // simply remove from list instead of involving engine + removeFromList(txn, contactGroup, TO_BE_SHARED_BY_US, f); + // TODO here we could also remove the old session state + // and invitation message + } + } + + } + + private SS initializeSharerState(Transaction txn, S f, + ContactId contactId) throws FormatException, DbException { + + Contact c = db.getContact(txn, contactId); + Group group = getContactGroup(c); + + // create local message to keep engine state + long now = clock.currentTimeMillis(); + Bytes salt = new Bytes(new byte[SHARING_SALT_LENGTH]); + random.nextBytes(salt.getBytes()); + Message m = clientHelper.createMessage(localGroup.getId(), now, + BdfList.of(salt)); + SessionId sessionId = new SessionId(m.getId().getBytes()); + + SS s = getSSFactory().build(sessionId, m.getId(), group.getId(), + SharerSessionState.State.PREPARE_INVITATION, contactId, f); + + // save local state to database + BdfDictionary d = s.toBdfDictionary(); + clientHelper.addLocalMessage(txn, m, d, false); + + return s; + } + + private IS initializeInviteeState(Transaction txn, + ContactId contactId, I msg, MessageId id) + throws FormatException, DbException { + + Contact c = db.getContact(txn, contactId); + Group group = getContactGroup(c); + S f = getSFactory().parse(msg); + + // create local message to keep engine state + long now = clock.currentTimeMillis(); + Bytes mSalt = new Bytes(new byte[SHARING_SALT_LENGTH]); + random.nextBytes(mSalt.getBytes()); + Message m = clientHelper.createMessage(localGroup.getId(), now, + BdfList.of(mSalt)); + + IS s = getISFactory() + .build(msg.getSessionId(), m.getId(), group.getId(), + InviteeSessionState.State.AWAIT_INVITATION, contactId, + f, id); + + // save local state to database + BdfDictionary d = s.toBdfDictionary(); + clientHelper.addLocalMessage(txn, m, d, false); + + return s; + } + + private SharingSessionState getSessionState(Transaction txn, + SessionId sessionId, boolean warn) + throws DbException, FormatException { + + try { + return getSessionStateForSharer(txn, sessionId); + } catch (NoSuchMessageException e) { + // State not found directly, so query for state for invitee + BdfDictionary query = BdfDictionary.of( + new BdfEntry(SESSION_ID, sessionId) + ); + + Map<MessageId, BdfDictionary> map = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId(), + query); + + if (map.size() > 1 && LOG.isLoggable(WARNING)) { + LOG.warning( + "More than one session state found for message with session ID " + + Arrays.hashCode(sessionId.getBytes())); + } + if (map.isEmpty()) { + if (warn && LOG.isLoggable(WARNING)) { + LOG.warning( + "No session state found for message with session ID " + + Arrays.hashCode(sessionId.getBytes())); + } + throw new FormatException(); + } + return SharingSessionState + .fromBdfDictionary(getISFactory(), getSSFactory(), + map.values().iterator().next()); + } + } + + private SS getSessionStateForSharer(Transaction txn, + SessionId sessionId) + throws DbException, FormatException { + + // we should be able to get the sharer state directly from sessionId + MessageId storageId = new MessageId(sessionId.getBytes()); + BdfDictionary d = + clientHelper.getMessageMetadataAsDictionary(txn, storageId); + + if (!d.getBoolean(IS_SHARER)) throw new FormatException(); + + return (SS) SharingSessionState + .fromBdfDictionary(getISFactory(), getSSFactory(), d); + } + + private IS getSessionStateForResponse(Transaction txn, + S f, Contact c) throws DbException, FormatException { + + // query for invitee states for that shareable in state await response + BdfDictionary query = BdfDictionary.of( + new BdfEntry(IS_SHARER, false), + new BdfEntry(CONTACT_ID, c.getId().getInt()), + new BdfEntry(SHAREABLE_ID, f.getId()), + new BdfEntry(STATE, AWAIT_LOCAL_RESPONSE.getValue()) + ); + + Map<MessageId, BdfDictionary> map = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId(), query); + + if (map.size() > 1 && LOG.isLoggable(WARNING)) { + LOG.warning( + "More than one session state found for shareable with ID " + + Arrays.hashCode(f.getId().getBytes()) + + " in state AWAIT_LOCAL_RESPONSE for contact " + + c.getAuthor().getName()); + } + if (map.isEmpty()) { + if (LOG.isLoggable(WARNING)) { + LOG.warning( + "No session state found for shareable with ID " + + Arrays.hashCode(f.getId().getBytes()) + + " in state AWAIT_LOCAL_RESPONSE"); + } + throw new DbException(); + } + return (IS) SharingSessionState + .fromBdfDictionary(getISFactory(), getSSFactory(), + map.values().iterator().next()); + } + + private SharingSessionState getSessionStateForLeaving(Transaction txn, + S f, ContactId c) throws DbException, FormatException { + + BdfDictionary query = BdfDictionary.of( + new BdfEntry(CONTACT_ID, c.getInt()), + new BdfEntry(SHAREABLE_ID, f.getId()) + ); + Map<MessageId, BdfDictionary> map = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId(), query); + for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { + BdfDictionary d = m.getValue(); + try { + SharingSessionState s = SharingSessionState + .fromBdfDictionary(getISFactory(), getSSFactory(), d); + + // check that a shareable get be left in current session + if (s instanceof SharerSessionState) { + SharerSessionState state = (SharerSessionState) s; + SharerSessionState.State nextState = + state.getState() + .next(SharerSessionState.Action.LOCAL_LEAVE); + if (nextState != SharerSessionState.State.ERROR) { + return state; + } + } else { + InviteeSessionState state = (InviteeSessionState) s; + InviteeSessionState.State nextState = state.getState() + .next(InviteeSessionState.Action.LOCAL_LEAVE); + if (nextState != InviteeSessionState.State.ERROR) { + return state; + } + } + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + throw new FormatException(); + } + + private void processStateUpdate(Transaction txn, + @Nullable MessageId messageId, + StateUpdate<SharingSessionState, BaseMessage> result, S f) + throws DbException, FormatException { + + // perform actions based on new local state + performTasks(txn, result.localState, f); + + // save new local state + MessageId storageId = result.localState.getStorageId(); + clientHelper.mergeMessageMetadata(txn, storageId, + result.localState.toBdfDictionary()); + + // send messages + for (BaseMessage msg : result.toSend) { + sendMessage(txn, msg); + } + + // broadcast events + for (Event event : result.toBroadcast) { + txn.attach(event); + } + + // delete message + if (result.deleteMessage && messageId != null) { + if (LOG.isLoggable(INFO)) { + LOG.info("Deleting message with id " + messageId.hashCode()); + } + db.deleteMessage(txn, messageId); + db.deleteMessageMetadata(txn, messageId); + } + } + + private void processSharerStateUpdate(Transaction txn, + @Nullable MessageId messageId, StateUpdate<SS, BaseMessage> result) + throws DbException, FormatException { + + StateUpdate<SharingSessionState, BaseMessage> r = + new StateUpdate<SharingSessionState, BaseMessage>( + result.deleteMessage, result.deleteState, + result.localState, result.toSend, result.toBroadcast); + + // get shareable for later + S f = getSFactory().parse(result.localState); + + processStateUpdate(txn, messageId, r, f); + } + + private void processInviteeStateUpdate(Transaction txn, + @Nullable MessageId messageId, StateUpdate<IS, BaseMessage> result) + throws DbException, FormatException { + + StateUpdate<SharingSessionState, BaseMessage> r = + new StateUpdate<SharingSessionState, BaseMessage>( + result.deleteMessage, result.deleteState, + result.localState, result.toSend, result.toBroadcast); + + // get shareable for later + S f = getSFactory().parse(result.localState); + + processStateUpdate(txn, messageId, r, f); + } + + private void performTasks(Transaction txn, SharingSessionState localState, + S f) throws FormatException, DbException { + + if (localState.getTask() == -1) return; + + // remember task and remove it from localState + long task = localState.getTask(); + localState.setTask(-1); + + // get group ID for later + GroupId groupId = localState.getContactGroupId(); + // get contact ID for later + ContactId contactId = localState.getContactId(); + + // perform tasks + if (task == TASK_ADD_SHAREABLE_TO_LIST_SHARED_WITH_US) { + addToList(txn, groupId, SHARED_WITH_US, f); + } else if (task == TASK_REMOVE_SHAREABLE_FROM_LIST_SHARED_WITH_US) { + removeFromList(txn, groupId, SHARED_WITH_US, f); + } else if (task == TASK_ADD_SHARED_SHAREABLE) { + // TODO we might want to call the add() method of the respective + // manager here, because blogs add a description for example + db.addGroup(txn, f.getGroup()); + db.setGroupVisibility(txn, contactId, f.getId(), SHARED); + } else if (task == TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US) { + addToList(txn, groupId, TO_BE_SHARED_BY_US, f); + } else if (task == TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US) { + removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f); + } else if (task == TASK_SHARE_SHAREABLE) { + db.setGroupVisibility(txn, contactId, f.getId(), SHARED); + removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f); + addToList(txn, groupId, SHARED_BY_US, f); + } else if (task == TASK_UNSHARE_SHAREABLE_SHARED_BY_US) { + db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE); + removeFromList(txn, groupId, SHARED_BY_US, f); + // broadcast event informing UI that contact has left the group + ContactLeftShareableEvent + e = new ContactLeftShareableEvent(f.getId(), contactId); + txn.attach(e); + } else if (task == TASK_UNSHARE_SHAREABLE_SHARED_WITH_US) { + db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE); + removeFromList(txn, groupId, SHARED_WITH_US, f); + // broadcast event informing UI that contact has left the group + ContactLeftShareableEvent + e = new ContactLeftShareableEvent(f.getId(), contactId); + txn.attach(e); + } + } + + private void sendMessage(Transaction txn, BaseMessage m) + throws FormatException, DbException { + + byte[] body = clientHelper.toByteArray(m.toBdfList()); + Group group = db.getGroup(txn, m.getGroupId()); + + // add message itself as metadata + BdfDictionary d = m.toBdfDictionary(); + d.put(LOCAL, true); + d.put(TIME, m.getTime()); + Metadata meta = metadataEncoder.encode(d); + + messageQueueManager + .sendMessage(txn, group, m.getTime(), body, meta); + } + + @Override + public Group getContactGroup(Contact c) { + return contactGroupFactory.createContactGroup(getClientId(), c); + } + + private ContactId getContactId(Transaction txn, GroupId contactGroupId) + throws DbException, FormatException { + BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn, + contactGroupId); + return new ContactId(meta.getLong(CONTACT_ID).intValue()); + } + + private void leaveShareable(Transaction txn, ContactId c, S f) + throws DbException, FormatException { + + SharingSessionState state = getSessionStateForLeaving(txn, f, c); + if (state instanceof SharerSessionState) { + SharerSessionState.Action action = + SharerSessionState.Action.LOCAL_LEAVE; + SharerEngine<I, SS, IRR> engine = + new SharerEngine<I, SS, IRR>(getIFactory(), + getIRRFactory(), clock); + processSharerStateUpdate(txn, null, + engine.onLocalAction((SS) state, action)); + } else { + InviteeSessionState.Action action = + InviteeSessionState.Action.LOCAL_LEAVE; + InviteeEngine<IS, IR> engine = + new InviteeEngine<IS, IR>(getIRFactory(), clock); + processInviteeStateUpdate(txn, null, + engine.onLocalAction((IS) state, action)); + } + } + + private boolean listContains(Transaction txn, GroupId contactGroup, + GroupId shareable, String key) throws DbException, FormatException { + + List<S> list = getShareableList(txn, contactGroup, key); + for (S f : list) { + if (f.getId().equals(shareable)) return true; + } + return false; + } + + private boolean addToList(Transaction txn, GroupId groupId, String key, + S f) throws DbException, FormatException { + + List<S> shareables = getShareableList(txn, groupId, key); + if (shareables.contains(f)) return false; + shareables.add(f); + storeShareableList(txn, groupId, key, shareables); + return true; + } + + private boolean removeFromList(Transaction txn, GroupId groupId, String key, + S f) throws DbException, FormatException { + + List<S> shareables = getShareableList(txn, groupId, key); + if (shareables.remove(f)) { + storeShareableList(txn, groupId, key, shareables); + return true; + } + return false; + } + + private List<S> getShareableList(Transaction txn, GroupId groupId, + String key) throws DbException, FormatException { + + BdfDictionary metadata = + clientHelper.getGroupMetadataAsDictionary(txn, groupId); + BdfList list = metadata.getList(key); + + return parseShareableList(list); + } + + private void storeShareableList(Transaction txn, GroupId groupId, + String key, + List<S> shareables) throws DbException, FormatException { + + BdfList list = encodeShareableList(shareables); + BdfDictionary metadata = BdfDictionary.of( + new BdfEntry(key, list) + ); + clientHelper.mergeGroupMetadata(txn, groupId, metadata); + } + + private BdfList encodeShareableList(List<S> shareables) { + BdfList shareableList = new BdfList(); + for (S f : shareables) + shareableList.add(getSFactory().encode(f)); + return shareableList; + } + + private List<S> parseShareableList(BdfList list) throws FormatException { + List<S> shareables = new ArrayList<S>(list.size()); + for (int i = 0; i < list.size(); i++) { + BdfList shareable = list.getList(i); + shareables.add(getSFactory().parse(shareable)); + } + return shareables; + } + + private void deleteMessage(Transaction txn, MessageId messageId) + throws DbException { + + if (LOG.isLoggable(INFO)) + LOG.info("Deleting message with ID: " + messageId.hashCode()); + + db.deleteMessage(txn, messageId); + db.deleteMessageMetadata(txn, messageId); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..4277303c628cdaf5581e325325ca3f82a7e98fcb --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngine.java @@ -0,0 +1,39 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.api.sharing.Shareable; + +import javax.annotation.Nullable; + +@NotNullByDefault +interface ProtocolEngine<S extends Shareable> { + + Session onInviteAction(Transaction txn, Session session, + @Nullable String message, long timestamp) throws DbException; + + Session onAcceptAction(Transaction txn, Session session) throws DbException; + + Session onDeclineAction(Transaction txn, Session session) + throws DbException; + + Session onLeaveAction(Transaction txn, Session session) throws DbException; + + Session onInviteMessage(Transaction txn, Session session, + InviteMessage<S> m) throws DbException, FormatException; + + Session onAcceptMessage(Transaction txn, Session session, AcceptMessage m) + throws DbException, FormatException; + + Session onDeclineMessage(Transaction txn, Session session, DeclineMessage m) + throws DbException, FormatException; + + Session onLeaveMessage(Transaction txn, Session session, LeaveMessage m) + throws DbException, FormatException; + + Session onAbortMessage(Transaction txn, Session session, AbortMessage m) + throws DbException, FormatException; + +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..dd8132d90aec63b30b489e3e3f55934697be584c --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ProtocolEngineImpl.java @@ -0,0 +1,619 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +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.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.ClientId; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.Group.Visibility; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.ProtocolStateException; +import org.briarproject.briar.api.sharing.Shareable; + +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; +import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; +import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; +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.SharingConstants.GROUP_KEY_CONTACT_ID; +import static org.briarproject.briar.sharing.State.ERROR; +import static org.briarproject.briar.sharing.State.LOCAL_INVITED; +import static org.briarproject.briar.sharing.State.LOCAL_LEFT; +import static org.briarproject.briar.sharing.State.REMOTE_HANGING; +import static org.briarproject.briar.sharing.State.REMOTE_INVITED; +import static org.briarproject.briar.sharing.State.REMOTE_LEFT; +import static org.briarproject.briar.sharing.State.SHARING; +import static org.briarproject.briar.sharing.State.START; + +@Immutable +@NotNullByDefault +abstract class ProtocolEngineImpl<S extends Shareable> + implements ProtocolEngine<S> { + + protected final DatabaseComponent db; + protected final ClientHelper clientHelper; + protected final MessageParser<S> messageParser; + + private final MessageEncoder messageEncoder; + private final MessageTracker messageTracker; + private final Clock clock; + + ProtocolEngineImpl(DatabaseComponent db, ClientHelper clientHelper, + MessageEncoder messageEncoder, MessageParser<S> messageParser, + MessageTracker messageTracker, Clock clock) { + this.db = db; + this.clientHelper = clientHelper; + this.messageEncoder = messageEncoder; + this.messageParser = messageParser; + this.messageTracker = messageTracker; + this.clock = clock; + } + + @Override + public Session onInviteAction(Transaction txn, Session s, + @Nullable String message, long timestamp) throws DbException { + switch (s.getState()) { + case START: + case REMOTE_LEFT: + return onLocalInvite(txn, s, message, timestamp); + case LOCAL_INVITED: + case REMOTE_INVITED: + case SHARING: + case LOCAL_LEFT: + case REMOTE_HANGING: + case ERROR: + throw new ProtocolStateException(); // Invalid in these states + default: + throw new AssertionError(); + } + } + + private Session onLocalInvite(Transaction txn, Session s, + @Nullable String message, long timestamp) throws DbException { + // Send an INVITE message + Message sent = sendInviteMessage(txn, s, message, timestamp); + // Track the message + messageTracker.trackOutgoingMessage(txn, sent); + // Make the shareable visible to the contact + try { + setShareableVisibility(txn, s, VISIBLE); + } catch (FormatException e) { + throw new DbException(e); // Invalid group metadata + } + // Move to the REMOTE_INVITED state + return new Session(REMOTE_INVITED, s.getContactGroupId(), + s.getShareableId(), sent.getId(), s.getLastRemoteMessageId(), + sent.getTimestamp(), s.getInviteTimestamp()); + } + + private Message sendInviteMessage(Transaction txn, Session s, + @Nullable String message, long timestamp) throws DbException { + Group g = db.getGroup(txn, s.getShareableId()); + BdfList descriptor; + try { + descriptor = clientHelper.toList(g.getDescriptor()); + } catch (FormatException e) { + throw new DbException(e); // Invalid group descriptor + } + long localTimestamp = Math.max(timestamp, getLocalTimestamp(s)); + Message m = messageEncoder + .encodeInviteMessage(s.getContactGroupId(), localTimestamp, + s.getLastLocalMessageId(), descriptor, message); + sendMessage(txn, m, INVITE, s.getShareableId(), true); + return m; + } + + @Override + public Session onAcceptAction(Transaction txn, Session s) + throws DbException { + switch (s.getState()) { + case LOCAL_INVITED: + return onLocalAccept(txn, s); + case START: + case REMOTE_INVITED: + case SHARING: + case LOCAL_LEFT: + case REMOTE_LEFT: + case REMOTE_HANGING: + case ERROR: + throw new ProtocolStateException(); // Invalid in these states + default: + throw new AssertionError(); + } + } + + private Session onLocalAccept(Transaction txn, Session s) + 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 ACCEPT message + Message sent = sendAcceptMessage(txn, s); + // Track the message + messageTracker.trackOutgoingMessage(txn, sent); + try { + // Add and subscribe to the shareable + addShareable(txn, inviteId); + // Share the shareable with the contact + setShareableVisibility(txn, s, SHARED); + } catch (FormatException e) { + throw new DbException(e); // Invalid group metadata + } + // Move to the SHARING state + return new Session(SHARING, s.getContactGroupId(), s.getShareableId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + s.getInviteTimestamp()); + } + + protected abstract void addShareable(Transaction txn, MessageId inviteId) + throws DbException, FormatException; + + private Message sendAcceptMessage(Transaction txn, Session session) + throws DbException { + Message m = messageEncoder.encodeAcceptMessage( + session.getContactGroupId(), session.getShareableId(), + getLocalTimestamp(session), session.getLastLocalMessageId()); + sendMessage(txn, m, ACCEPT, session.getShareableId(), true); + return m; + } + + @Override + public Session onDeclineAction(Transaction txn, Session s) + throws DbException { + switch (s.getState()) { + case LOCAL_INVITED: + return onLocalDecline(txn, s); + case START: + case REMOTE_INVITED: + case SHARING: + case LOCAL_LEFT: + case REMOTE_LEFT: + case REMOTE_HANGING: + case ERROR: + throw new ProtocolStateException(); // Invalid in these states + default: + throw new AssertionError(); + } + } + + private Session onLocalDecline(Transaction txn, Session s) + 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 DECLINE message + Message sent = sendDeclineMessage(txn, s); + // Track the message + messageTracker.trackOutgoingMessage(txn, sent); + // Move to the START state + return new Session(START, s.getContactGroupId(), s.getShareableId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + s.getInviteTimestamp()); + } + + private Message sendDeclineMessage(Transaction txn, Session session) + throws DbException { + Message m = messageEncoder.encodeDeclineMessage( + session.getContactGroupId(), session.getShareableId(), + getLocalTimestamp(session), session.getLastLocalMessageId()); + sendMessage(txn, m, DECLINE, session.getShareableId(), true); + return m; + } + + @Override + public Session onLeaveAction(Transaction txn, Session s) + throws DbException { + switch (s.getState()) { + case REMOTE_INVITED: + return onLocalLeave(txn, s, REMOTE_HANGING); + case SHARING: + return onLocalLeave(txn, s, LOCAL_LEFT); + case REMOTE_LEFT: + return onLocalLeave(txn, s, START); + case START: + case LOCAL_INVITED: + case LOCAL_LEFT: + case REMOTE_HANGING: + case ERROR: + return s; // Ignored in this state + default: + throw new AssertionError(); + } + } + + private Session onLocalLeave(Transaction txn, Session s, State nextState) + throws DbException { + try { + // Stop sharing the shareable (not actually needed in REMOTE_LEFT) + setShareableVisibility(txn, s, INVISIBLE); + } catch (FormatException e) { + throw new DbException(e); // Invalid group metadata + } + // Send a LEAVE message + Message sent = sendLeaveMessage(txn, s); + // Move to the next state + return new Session(nextState, s.getContactGroupId(), s.getShareableId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + s.getInviteTimestamp()); + } + + private Message sendLeaveMessage(Transaction txn, Session session) + throws DbException { + Message m = messageEncoder.encodeLeaveMessage( + session.getContactGroupId(), session.getShareableId(), + getLocalTimestamp(session), session.getLastLocalMessageId()); + sendMessage(txn, m, LEAVE, session.getShareableId(), false); + return m; + } + + @Override + public Session onInviteMessage(Transaction txn, Session s, + InviteMessage<S> m) throws DbException, FormatException { + switch (s.getState()) { + case START: + case LOCAL_LEFT: + return onRemoteInvite(txn, s, m, true, LOCAL_INVITED); + case REMOTE_INVITED: + return onRemoteInviteWhenInvited(txn, s, m); + case REMOTE_HANGING: + return onRemoteInvite(txn, s, m, false, LOCAL_LEFT); + case LOCAL_INVITED: + case SHARING: + case REMOTE_LEFT: + return abort(txn, s); // Invalid in these states + case ERROR: + return s; // Ignored in this state + default: + throw new AssertionError(); + } + } + + private Session onRemoteInvite(Transaction txn, Session s, + InviteMessage<S> m, boolean available, State nextState) + throws DbException, FormatException { + // The timestamp must be higher than the last invite message, if any + if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s); + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Mark the invite message visible in the UI and (un)available to answer + markMessageVisibleInUi(txn, m.getId(), true); + markMessageAvailableToAnswer(txn, m.getId(), available); + // Track the message + messageTracker.trackMessage(txn, m.getContactGroupId(), + m.getTimestamp(), false); + // Broadcast an event + ContactId contactId = getContactId(txn, s.getContactGroupId()); + txn.attach(getInvitationRequestReceivedEvent(m, contactId, available, + false)); + // Move to the next state + return new Session(nextState, s.getContactGroupId(), s.getShareableId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + m.getTimestamp()); + } + + private Session onRemoteInviteWhenInvited(Transaction txn, Session s, + InviteMessage<S> m) throws DbException, FormatException { + // The timestamp must be higher than the last invite message, if any + if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s); + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Mark the invite message visible in the UI and unavailable to answer + markMessageVisibleInUi(txn, m.getId(), true); + markMessageAvailableToAnswer(txn, m.getId(), false); + // Track the message + messageTracker.trackMessage(txn, m.getContactGroupId(), + m.getTimestamp(), false); + // Share the shareable with the contact + setShareableVisibility(txn, s, SHARED); + // Broadcast an event + ContactId contactId = getContactId(txn, s.getContactGroupId()); + txn.attach( + getInvitationRequestReceivedEvent(m, contactId, false, true)); + // Move to the next state + return new Session(SHARING, s.getContactGroupId(), s.getShareableId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + m.getTimestamp()); + } + + abstract Event getInvitationRequestReceivedEvent(InviteMessage<S> m, + ContactId contactId, boolean available, boolean canBeOpened); + + @Override + public Session onAcceptMessage(Transaction txn, Session s, + AcceptMessage m) throws DbException, FormatException { + switch (s.getState()) { + case REMOTE_INVITED: + return onRemoteAcceptWhenInvited(txn, s, m); + case REMOTE_HANGING: + return onRemoteAccept(txn, s, m, LOCAL_LEFT); + case START: + case LOCAL_INVITED: + case SHARING: + case LOCAL_LEFT: + case REMOTE_LEFT: + return abort(txn, s); // Invalid in these states + case ERROR: + return s; // Ignored in this state + default: + throw new AssertionError(); + } + } + + private Session onRemoteAccept(Transaction txn, Session s, AcceptMessage m, + State nextState) throws DbException, FormatException { + // The timestamp must be higher than the last invite message + if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s); + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Mark the response visible in the UI + markMessageVisibleInUi(txn, m.getId(), true); + // Track the message + messageTracker.trackMessage(txn, m.getContactGroupId(), + m.getTimestamp(), false); + // Broadcast an event + ContactId contactId = getContactId(txn, m.getContactGroupId()); + txn.attach(getInvitationResponseReceivedEvent(m, contactId)); + // Move to the next state + return new Session(nextState, s.getContactGroupId(), s.getShareableId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + s.getInviteTimestamp()); + } + + private Session onRemoteAcceptWhenInvited(Transaction txn, Session s, + AcceptMessage m) throws DbException, FormatException { + // Perform normal remote accept validation and operation + Session session = onRemoteAccept(txn, s, m, SHARING); + // Share the shareable with the contact + if (session.getState() != ERROR) + setShareableVisibility(txn, s, SHARED); + return session; + } + + abstract Event getInvitationResponseReceivedEvent(AcceptMessage m, + ContactId contactId); + + @Override + public Session onDeclineMessage(Transaction txn, Session s, + DeclineMessage m) throws DbException, FormatException { + switch (s.getState()) { + case REMOTE_INVITED: + case REMOTE_HANGING: + return onRemoteDecline(txn, s, m); + case START: + case LOCAL_INVITED: + case SHARING: + case LOCAL_LEFT: + case REMOTE_LEFT: + return abort(txn, s); // Invalid in these states + case ERROR: + return s; // Ignored in this state + default: + throw new AssertionError(); + } + } + + private Session onRemoteDecline(Transaction txn, Session s, + DeclineMessage m) throws DbException, FormatException { + // The timestamp must be higher than the last invite message + if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s); + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Mark the response visible in the UI + markMessageVisibleInUi(txn, m.getId(), true); + // Track the message + messageTracker.trackMessage(txn, m.getContactGroupId(), + m.getTimestamp(), false); + // Make the shareable invisible (not actually needed in REMOTE_HANGING) + try { + setShareableVisibility(txn, s, INVISIBLE); + } catch (FormatException e) { + throw new DbException(e); // Invalid group metadata + } + // Broadcast an event + ContactId contactId = getContactId(txn, m.getContactGroupId()); + txn.attach(getInvitationResponseReceivedEvent(m, contactId)); + // Move to the next state + return new Session(START, s.getContactGroupId(), s.getShareableId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + s.getInviteTimestamp()); + } + + abstract Event getInvitationResponseReceivedEvent(DeclineMessage m, + ContactId contactId); + + @Override + public Session onLeaveMessage(Transaction txn, Session s, + LeaveMessage m) throws DbException, FormatException { + switch (s.getState()) { + case LOCAL_INVITED: + return onRemoteLeaveWhenInvited(txn, s, m, START); + case LOCAL_LEFT: + return onRemoteLeave(txn, s, m, START); + case SHARING: + return onRemoteLeaveWhenSharing(txn, s, m); + case START: + case REMOTE_INVITED: + case REMOTE_LEFT: + case REMOTE_HANGING: + return abort(txn, s); // Invalid in these states + case ERROR: + return s; // Ignored in this state + default: + throw new AssertionError(); + } + } + + private Session onRemoteLeaveWhenInvited(Transaction txn, Session s, + LeaveMessage m, State nextState) + throws DbException, FormatException { + // Carry out normal leave validation and operation + Session session = onRemoteLeave(txn, s, m, nextState); + // Mark any invite messages in the session unavailable to answer + if (session.getState() != ERROR) + markInvitesUnavailableToAnswer(txn, s); + // Move to the next state + return session; + } + + private Session onRemoteLeave(Transaction txn, Session s, + LeaveMessage m, State nextState) + throws DbException, FormatException { + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Move to the next state + return new Session(nextState, s.getContactGroupId(), s.getShareableId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + s.getInviteTimestamp()); + } + + private Session onRemoteLeaveWhenSharing(Transaction txn, Session s, + LeaveMessage m) throws DbException, FormatException { + // Carry out normal leave validation and operation + Session session = onRemoteLeave(txn, s, m, REMOTE_LEFT); + // Stop sharing the shareable with the contact + if (session.getState() != ERROR) + setShareableVisibility(txn, s, INVISIBLE); + // Move to the next state + return session; + } + + @Override + public Session onAbortMessage(Transaction txn, Session s, AbortMessage m) + throws DbException, FormatException { + return abort(txn, s); + } + + private Session abort(Transaction txn, Session s) + throws DbException, FormatException { + // If the session has already been aborted, do nothing + if (s.getState() == ERROR) return s; + // Mark any invite messages in the session unavailable to answer + markInvitesUnavailableToAnswer(txn, s); + // If we subscribe, make the shareable invisible to the contact + if (isSubscribed(txn, s.getShareableId())) + setShareableVisibility(txn, s, INVISIBLE); + // Send an ABORT message + Message sent = sendAbortMessage(txn, s); + // Move to the ERROR state + return new Session(ERROR, s.getContactGroupId(), s.getShareableId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + s.getInviteTimestamp()); + } + + private void markInvitesUnavailableToAnswer(Transaction txn, Session s) + throws DbException, FormatException { + GroupId shareableId = s.getShareableId(); + BdfDictionary query = + messageParser.getInvitesAvailableToAnswerQuery(shareableId); + Map<MessageId, BdfDictionary> results = + clientHelper.getMessageMetadataAsDictionary(txn, + s.getContactGroupId(), query); + for (MessageId m : results.keySet()) + markMessageAvailableToAnswer(txn, m, false); + } + + private boolean isSubscribed(Transaction txn, GroupId g) + throws DbException { + if (!db.containsGroup(txn, g)) return false; + Group group = db.getGroup(txn, g); + return group.getClientId().equals(getClientId()); + } + + protected abstract ClientId getClientId(); + + private Message sendAbortMessage(Transaction txn, Session session) + throws DbException { + Message m = messageEncoder.encodeAbortMessage( + session.getContactGroupId(), session.getShareableId(), + getLocalTimestamp(session), session.getLastLocalMessageId()); + sendMessage(txn, m, ABORT, session.getShareableId(), false); + return m; + } + + private void sendMessage(Transaction txn, Message m, MessageType type, + GroupId shareableId, boolean visibleInConversation) + throws DbException { + BdfDictionary meta = messageEncoder.encodeMetadata(type, shareableId, + m.getTimestamp(), true, true, visibleInConversation, false); + try { + clientHelper.addLocalMessage(txn, m, meta, true); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + private void markMessageAvailableToAnswer(Transaction txn, MessageId m, + boolean available) throws DbException { + BdfDictionary meta = new BdfDictionary(); + messageEncoder.setAvailableToAnswer(meta, available); + try { + clientHelper.mergeMessageMetadata(txn, m, meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + private void markMessageVisibleInUi(Transaction txn, MessageId m, + boolean visible) throws DbException { + BdfDictionary meta = new BdfDictionary(); + messageEncoder.setVisibleInUi(meta, visible); + try { + clientHelper.mergeMessageMetadata(txn, m, meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + private void setShareableVisibility(Transaction txn, Session session, + Visibility v) throws DbException, FormatException { + ContactId contactId = getContactId(txn, session.getContactGroupId()); + db.setGroupVisibility(txn, contactId, session.getShareableId(), v); + } + + private ContactId getContactId(Transaction txn, GroupId contactGroupId) + throws DbException, FormatException { + BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn, + contactGroupId); + return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue()); + } + + private boolean isValidDependency(Session session, + @Nullable MessageId dependency) { + MessageId expected = session.getLastRemoteMessageId(); + if (dependency == null) return expected == null; + return expected != null && dependency.equals(expected); + } + + private long getLocalTimestamp(Session session) { + return Math.max(clock.currentTimeMillis(), + Math.max(session.getLocalTimestamp(), + session.getInviteTimestamp()) + 1); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/Session.java b/briar-core/src/main/java/org/briarproject/briar/sharing/Session.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0a5520180d7b3ea984d8b098f324953e109ae0 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/Session.java @@ -0,0 +1,69 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.briar.sharing.State.START; + +@Immutable +@NotNullByDefault +class Session { + + private final State state; + private final GroupId contactGroupId, shareableId; + @Nullable + private final MessageId lastLocalMessageId, lastRemoteMessageId; + private final long localTimestamp, inviteTimestamp; + + Session(State state, GroupId contactGroupId, GroupId shareableId, + @Nullable MessageId lastLocalMessageId, + @Nullable MessageId lastRemoteMessageId, long localTimestamp, + long inviteTimestamp) { + this.state = state; + this.contactGroupId = contactGroupId; + this.shareableId = shareableId; + this.lastLocalMessageId = lastLocalMessageId; + this.lastRemoteMessageId = lastRemoteMessageId; + this.localTimestamp = localTimestamp; + this.inviteTimestamp = inviteTimestamp; + } + + Session(GroupId contactGroupId, GroupId shareableId) { + this(START, contactGroupId, shareableId, null, null, 0, 0); + } + + public State getState() { + return state; + } + + GroupId getContactGroupId() { + return contactGroupId; + } + + GroupId getShareableId() { + return shareableId; + } + + @Nullable + MessageId getLastLocalMessageId() { + return lastLocalMessageId; + } + + @Nullable + MessageId getLastRemoteMessageId() { + return lastRemoteMessageId; + } + + long getLocalTimestamp() { + return localTimestamp; + } + + long getInviteTimestamp() { + return inviteTimestamp; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoder.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoder.java new file mode 100644 index 0000000000000000000000000000000000000000..61862e8936612d168f425ad1992b7871f244d145 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoder.java @@ -0,0 +1,11 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +interface SessionEncoder { + + BdfDictionary encodeSession(Session s); + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoderImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..7cf2bf139d67495092dec01dc158674aec217a63 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionEncoderImpl.java @@ -0,0 +1,46 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_INVITE_TIMESTAMP; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LOCAL_TIMESTAMP; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_SESSION_ID; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_SHAREABLE_ID; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_STATE; + +@Immutable +@NotNullByDefault +class SessionEncoderImpl implements SessionEncoder { + + @Inject + SessionEncoderImpl() { + } + + @Override + public BdfDictionary encodeSession(Session s) { + BdfDictionary d = new BdfDictionary(); + d.put(SESSION_KEY_SESSION_ID, s.getShareableId()); + d.put(SESSION_KEY_SHAREABLE_ID, s.getShareableId()); + MessageId lastLocalMessageId = s.getLastLocalMessageId(); + if (lastLocalMessageId == null) + d.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, NULL_VALUE); + else d.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, lastLocalMessageId); + MessageId lastRemoteMessageId = s.getLastRemoteMessageId(); + if (lastRemoteMessageId == null) + d.put(SESSION_KEY_LAST_REMOTE_MESSAGE_ID, NULL_VALUE); + else d.put(SESSION_KEY_LAST_REMOTE_MESSAGE_ID, lastRemoteMessageId); + d.put(SESSION_KEY_LOCAL_TIMESTAMP, s.getLocalTimestamp()); + d.put(SESSION_KEY_INVITE_TIMESTAMP, s.getInviteTimestamp()); + d.put(SESSION_KEY_STATE, s.getState().getValue()); + return d; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java new file mode 100644 index 0000000000000000000000000000000000000000..a82aed3eca199a2eb8eb2924375595a88ba42a0e --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParser.java @@ -0,0 +1,17 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.briar.api.client.SessionId; + +@NotNullByDefault +interface SessionParser { + + BdfDictionary getSessionQuery(SessionId s); + + Session parseSession(GroupId contactGroupId, BdfDictionary d) + throws FormatException; + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..62d9c44b12b276ea27655471b916451597ceedcb --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SessionParserImpl.java @@ -0,0 +1,75 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +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 javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_INVITE_TIMESTAMP; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_LOCAL_TIMESTAMP; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_SESSION_ID; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_SHAREABLE_ID; +import static org.briarproject.briar.sharing.SharingConstants.SESSION_KEY_STATE; + +@Immutable +@NotNullByDefault +class SessionParserImpl implements SessionParser { + + @Inject + SessionParserImpl() { + } + + @Override + public BdfDictionary getSessionQuery(SessionId s) { + return BdfDictionary.of(new BdfEntry(SESSION_KEY_SESSION_ID, s)); + } + + @Override + public Session parseSession(GroupId contactGroupId, + BdfDictionary d) throws FormatException { + return new Session(State.fromValue(getState(d)), contactGroupId, + getShareableId(d), getLastLocalMessageId(d), + getLastRemoteMessageId(d), getLocalTimestamp(d), + getInviteTimestamp(d)); + } + + private int getState(BdfDictionary d) throws FormatException { + return d.getLong(SESSION_KEY_STATE).intValue(); + } + + private GroupId getShareableId(BdfDictionary d) throws FormatException { + return new GroupId(d.getRaw(SESSION_KEY_SHAREABLE_ID)); + } + + @Nullable + private MessageId getLastLocalMessageId(BdfDictionary d) + throws FormatException { + byte[] b = d.getOptionalRaw(SESSION_KEY_LAST_LOCAL_MESSAGE_ID); + return b == null ? null : new MessageId(b); + } + + @Nullable + private MessageId getLastRemoteMessageId(BdfDictionary d) + throws FormatException { + byte[] b = d.getOptionalRaw(SESSION_KEY_LAST_REMOTE_MESSAGE_ID); + return b == null ? null : new MessageId(b); + } + + private long getLocalTimestamp(BdfDictionary d) throws FormatException { + return d.getLong(SESSION_KEY_LOCAL_TIMESTAMP); + } + + private long getInviteTimestamp(BdfDictionary d) throws FormatException { + return d.getLong(SESSION_KEY_INVITE_TIMESTAMP); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ShareableFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ShareableFactory.java index 2d18d56ceda10d736f8bfab22d6c3af2799521cb..367f8882abaa94418b053bbf7113508bd2cccc8e 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/ShareableFactory.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ShareableFactory.java @@ -9,6 +9,7 @@ import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.briar.api.sharing.Shareable; import org.briarproject.briar.api.sharing.SharingMessage; +@Deprecated @NotNullByDefault interface ShareableFactory<S extends Shareable, I extends SharingMessage.Invitation, IS extends InviteeSessionState, SS extends SharerSessionState> { diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerEngine.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerEngine.java index 7d5b351834557775d4f7a634400a5f6514938813..eb05d87cdffc6b91aaa773bb7f5fc463a9d24f90 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerEngine.java @@ -29,6 +29,7 @@ import static org.briarproject.briar.api.sharing.SharingMessage.SimpleMessage; import static org.briarproject.briar.sharing.SharerSessionState.Action.REMOTE_ACCEPT; import static org.briarproject.briar.sharing.SharerSessionState.Action.REMOTE_DECLINE; +@Deprecated @Immutable @NotNullByDefault class SharerEngine<I extends Invitation, SS extends SharerSessionState, IRR extends InvitationResponseReceivedEvent> @@ -37,12 +38,12 @@ class SharerEngine<I extends Invitation, SS extends SharerSessionState, IRR exte private static final Logger LOG = Logger.getLogger(SharerEngine.class.getName()); - private final InvitationFactory<I, SS> invitationFactory; + private final OldInvitationFactory<I, SS> invitationFactory; private final InvitationResponseReceivedEventFactory<SS, IRR> invitationResponseReceivedEventFactory; private final Clock clock; - SharerEngine(InvitationFactory<I, SS> invitationFactory, + SharerEngine(OldInvitationFactory<I, SS> invitationFactory, InvitationResponseReceivedEventFactory<SS, IRR> invitationResponseReceivedEventFactory, Clock clock) { this.invitationFactory = invitationFactory; diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionState.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionState.java index 597c88c688d9f23e65ac3662317c14083bd150e5..2579a3f509023345c3168943e4deb91e6c754480 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionState.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionState.java @@ -23,6 +23,7 @@ import static org.briarproject.briar.sharing.SharerSessionState.Action.REMOTE_AC import static org.briarproject.briar.sharing.SharerSessionState.Action.REMOTE_DECLINE; import static org.briarproject.briar.sharing.SharerSessionState.Action.REMOTE_LEAVE; +@Deprecated @NotThreadSafe @NotNullByDefault public abstract class SharerSessionState extends SharingSessionState { diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionStateFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionStateFactory.java index a0a36f8f8995d90bcb16d18225e439082dd269ce..9be12463f78edd0dc85c3e033da3d54322ae36bf 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionStateFactory.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharerSessionStateFactory.java @@ -9,6 +9,7 @@ import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.sharing.Shareable; +@Deprecated @NotNullByDefault interface SharerSessionStateFactory<S extends Shareable, SS extends SharerSessionState> { diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingConstants.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..d860abc8f2b993e61adb46664c51575e41a9a546 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingConstants.java @@ -0,0 +1,28 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.briar.client.MessageTrackerConstants; + +interface SharingConstants { + + // Group metadata keys + String GROUP_KEY_CONTACT_ID = "contactId"; + + // Message metadata keys + String MSG_KEY_MESSAGE_TYPE = "messageType"; + String MSG_KEY_SHAREABLE_ID = "shareableId"; + String MSG_KEY_TIMESTAMP = "timestamp"; + String MSG_KEY_READ = MessageTrackerConstants.MSG_KEY_READ; + String MSG_KEY_LOCAL = "local"; + String MSG_KEY_VISIBLE_IN_UI = "visibleInUi"; + String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer"; + + // Session keys + String SESSION_KEY_STATE = "state"; + String SESSION_KEY_SESSION_ID = "sessionId"; + String SESSION_KEY_SHAREABLE_ID = "shareableId"; + String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId"; + String SESSION_KEY_LAST_REMOTE_MESSAGE_ID = "lastRemoteMessageId"; + String SESSION_KEY_LOCAL_TIMESTAMP = "localTimestamp"; + String SESSION_KEY_INVITE_TIMESTAMP = "inviteTimestamp"; + +} 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 c6be693e5d8b5e000ad35e88b1e7984058da49f2..e0c640e1e6258c1a8d4a905b14a3b840f1069a04 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,5 @@ package org.briarproject.briar.sharing; -import org.briarproject.bramble.api.Bytes; import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.client.ContactGroupFactory; @@ -9,17 +8,12 @@ import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactManager.AddContactHook; import org.briarproject.bramble.api.contact.ContactManager.RemoveContactHook; import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfEntry; import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.data.MetadataEncoder; import org.briarproject.bramble.api.data.MetadataParser; import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.Metadata; -import org.briarproject.bramble.api.db.NoSuchMessageException; import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.Client; import org.briarproject.bramble.api.sync.ClientId; @@ -28,127 +22,65 @@ import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageStatus; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.bramble.util.StringUtils; -import org.briarproject.briar.api.client.MessageQueueManager; import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.sharing.InvitationMessage; +import org.briarproject.briar.api.sharing.InvitationRequest; +import org.briarproject.briar.api.sharing.InvitationResponse; import org.briarproject.briar.api.sharing.Shareable; import org.briarproject.briar.api.sharing.SharingInvitationItem; import org.briarproject.briar.api.sharing.SharingManager; -import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent; -import org.briarproject.briar.api.sharing.event.InvitationRequestReceivedEvent; -import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent; import org.briarproject.briar.client.ConversationClientImpl; -import java.io.IOException; -import java.security.SecureRandom; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; +import java.util.Map.Entry; import javax.annotation.Nullable; -import static java.util.logging.Level.INFO; -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; -import static org.briarproject.briar.api.client.ProtocolEngine.StateUpdate; -import static org.briarproject.briar.api.sharing.SharingConstants.CONTACT_ID; -import static org.briarproject.briar.api.sharing.SharingConstants.IS_SHARER; -import static org.briarproject.briar.api.sharing.SharingConstants.LOCAL; -import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH; -import static org.briarproject.briar.api.sharing.SharingConstants.SESSION_ID; -import static org.briarproject.briar.api.sharing.SharingConstants.SHAREABLE_ID; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARED_BY_US; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARED_WITH_US; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARING_SALT_LENGTH; -import static org.briarproject.briar.api.sharing.SharingConstants.STATE; -import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHAREABLE_TO_LIST_SHARED_WITH_US; -import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US; -import static org.briarproject.briar.api.sharing.SharingConstants.TASK_ADD_SHARED_SHAREABLE; -import static org.briarproject.briar.api.sharing.SharingConstants.TASK_REMOVE_SHAREABLE_FROM_LIST_SHARED_WITH_US; -import static org.briarproject.briar.api.sharing.SharingConstants.TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US; -import static org.briarproject.briar.api.sharing.SharingConstants.TASK_SHARE_SHAREABLE; -import static org.briarproject.briar.api.sharing.SharingConstants.TASK_UNSHARE_SHAREABLE_SHARED_BY_US; -import static org.briarproject.briar.api.sharing.SharingConstants.TASK_UNSHARE_SHAREABLE_SHARED_WITH_US; -import static org.briarproject.briar.api.sharing.SharingConstants.TIME; -import static org.briarproject.briar.api.sharing.SharingConstants.TO_BE_SHARED_BY_US; -import static org.briarproject.briar.api.sharing.SharingConstants.TYPE; -import static org.briarproject.briar.api.sharing.SharingMessage.BaseMessage; -import static org.briarproject.briar.api.sharing.SharingMessage.Invitation; -import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; -import static org.briarproject.briar.sharing.InviteeSessionState.State.AWAIT_LOCAL_RESPONSE; +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.SharingConstants.GROUP_KEY_CONTACT_ID; @NotNullByDefault -abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS extends InviteeSessionState, SS extends SharerSessionState, IR extends InvitationRequestReceivedEvent, IRR extends InvitationResponseReceivedEvent> +abstract class SharingManagerImpl<S extends Shareable> extends ConversationClientImpl implements SharingManager<S>, Client, AddContactHook, RemoveContactHook { - private static final Logger LOG = - Logger.getLogger(SharingManagerImpl.class.getName()); - - private final MessageQueueManager messageQueueManager; - private final MetadataEncoder metadataEncoder; - private final SecureRandom random; + private final MessageParser<S> messageParser; + private final SessionEncoder sessionEncoder; + private final SessionParser sessionParser; private final ContactGroupFactory contactGroupFactory; - private final Clock clock; - private final Group localGroup; - - SharingManagerImpl(DatabaseComponent db, - MessageQueueManager messageQueueManager, ClientHelper clientHelper, - MetadataParser metadataParser, MetadataEncoder metadataEncoder, - SecureRandom random, ContactGroupFactory contactGroupFactory, - MessageTracker messageTracker, Clock clock) { + private final ProtocolEngine<S> engine; + private final InvitationFactory<S> invitationFactory; + + SharingManagerImpl(DatabaseComponent db, ClientHelper clientHelper, + MetadataParser metadataParser, MessageParser<S> messageParser, + SessionEncoder sessionEncoder, SessionParser sessionParser, + MessageTracker messageTracker, + ContactGroupFactory contactGroupFactory, ProtocolEngine<S> engine, + InvitationFactory<S> invitationFactory) { super(db, clientHelper, metadataParser, messageTracker); - this.messageQueueManager = messageQueueManager; - this.metadataEncoder = metadataEncoder; - this.random = random; + this.messageParser = messageParser; + this.sessionEncoder = sessionEncoder; + this.sessionParser = sessionParser; this.contactGroupFactory = contactGroupFactory; - this.clock = clock; - localGroup = contactGroupFactory.createLocalGroup(getClientId()); + this.engine = engine; + this.invitationFactory = invitationFactory; } protected abstract ClientId getClientId(); - protected abstract InvitationMessage createInvitationRequest(MessageId id, - I msg, ContactId contactId, GroupId shareableId, boolean available, - boolean canBeOpened, long time, boolean local, boolean sent, - boolean seen, boolean read); - - protected abstract InvitationMessage createInvitationResponse(MessageId id, - SessionId sessionId, GroupId groupId, ContactId contactId, - GroupId shareableId, boolean accept, long time, boolean local, - boolean sent, boolean seen, boolean read); - - protected abstract ShareableFactory<S, I, IS, SS> getSFactory(); - - protected abstract InvitationFactory<I, SS> getIFactory(); - - protected abstract InviteeSessionStateFactory<S, IS> getISFactory(); - - protected abstract SharerSessionStateFactory<S, SS> getSSFactory(); - - protected abstract InvitationReceivedEventFactory<IS, IR> getIRFactory(); - - protected abstract InvitationResponseReceivedEventFactory<SS, IRR> getIRRFactory(); - @Override public void createLocalState(Transaction txn) throws DbException { - db.addGroup(txn, localGroup); // Ensure we've set things up for any pre-existing contacts for (Contact c : db.getContacts(txn)) addingContact(txn, c); } @@ -165,10 +97,7 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED); // Attach the contact ID to the group BdfDictionary meta = new BdfDictionary(); - meta.put(CONTACT_ID, c.getId().getInt()); - meta.put(TO_BE_SHARED_BY_US, new BdfList()); - meta.put(SHARED_BY_US, new BdfList()); - meta.put(SHARED_WITH_US, new BdfList()); + meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt()); clientHelper.mergeGroupMetadata(txn, g.getId(), meta); } catch (FormatException e) { throw new DbException(e); @@ -177,152 +106,132 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS @Override public void removingContact(Transaction txn, Contact c) throws DbException { - // query for this contact c - BdfDictionary query = BdfDictionary.of( - new BdfEntry(CONTACT_ID, c.getId().getInt()) - ); - - // clean up session states with that contact from localGroup - try { - Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, localGroup.getId(), - query); - for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) { - deleteMessage(txn, entry.getKey()); - } - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - } - // remove the contact group (all messages will be removed with it) db.removeGroup(txn, getContactGroup(c)); } + @Override + public Group getContactGroup(Contact c) { + return contactGroupFactory.createContactGroup(getClientId(), c); + } + @Override protected boolean incomingMessage(Transaction txn, Message m, BdfList body, BdfDictionary d) throws DbException, FormatException { - - BaseMessage msg = BaseMessage.from(getIFactory(), m.getGroupId(), d); - SessionId sessionId = msg.getSessionId(); - - if (msg.getType() == SHARE_MSG_TYPE_INVITATION) { - // we are an invitee who just received a new invitation - boolean stateExists = true; - try { - // check if we have a session with that ID already - getSessionState(txn, sessionId, false); - } catch (FormatException e) { - // this is what we would expect under normal circumstances - stateExists = false; - } - // check if we already have a state with that sessionId - if (stateExists) throw new FormatException(); - - // check if shareable can be shared - I invitation = (I) msg; - S f = getSFactory().parse(invitation); - ContactId contactId = getContactId(txn, m.getGroupId()); - Contact contact = db.getContact(txn, contactId); - if (!canBeShared(txn, f.getId(), contact)) - checkForRaceCondition(txn, f, contact); - - // initialize state and process invitation - IS state = initializeInviteeState(txn, contactId, invitation, - m.getId()); - InviteeEngine<IS, IR> engine = - new InviteeEngine<IS, IR>(getIRFactory(), clock); - processInviteeStateUpdate(txn, m.getId(), - engine.onMessageReceived(state, msg)); - messageTracker.trackIncomingMessage(txn, m); - } else if (msg.getType() == SHARE_MSG_TYPE_ACCEPT || - msg.getType() == SHARE_MSG_TYPE_DECLINE) { - // we are a sharer who just received a response - SS state = getSessionStateForSharer(txn, sessionId); - state.setResponseId(m.getId()); - SharerEngine<I, SS, IRR> engine = - new SharerEngine<I, SS, IRR>(getIFactory(), - getIRRFactory(), clock); - processSharerStateUpdate(txn, m.getId(), - engine.onMessageReceived(state, msg)); - messageTracker.trackIncomingMessage(txn, m); - } else if (msg.getType() == SHARE_MSG_TYPE_LEAVE || - msg.getType() == SHARE_MSG_TYPE_ABORT) { - // we don't know who we are, so figure it out - SharingSessionState s = getSessionState(txn, sessionId, true); - if (s instanceof SharerSessionState) { - // we are a sharer and the invitee wants to leave or abort - SS state = (SS) s; - SharerEngine<I, SS, IRR> engine = - new SharerEngine<I, SS, IRR>(getIFactory(), - getIRRFactory(), clock); - processSharerStateUpdate(txn, m.getId(), - engine.onMessageReceived(state, msg)); - } else { - // we are an invitee and the sharer wants to leave or abort - IS state = (IS) s; - InviteeEngine<IS, IR> engine = - new InviteeEngine<IS, IR>(getIRFactory(), clock); - processInviteeStateUpdate(txn, m.getId(), - engine.onMessageReceived(state, msg)); - } + // Parse the metadata + MessageMetadata meta = messageParser.parseMetadata(d); + // Look up the session, if there is one + SessionId sessionId = getSessionId(meta.getShareableId()); + StoredSession ss = getSession(txn, m.getGroupId(), sessionId); + // Handle the message + Session session; + MessageId storageId; + if (ss == null) { + session = handleFirstMessage(txn, m, body, meta); + storageId = createStorageId(txn, m.getGroupId()); } else { - // message has passed validator, so that should never happen - throw new AssertionError("Illegal Sharing Message"); + session = handleMessage(txn, m, body, meta, ss.bdfSession); + storageId = ss.storageId; } - // don't share message as other party already has it + // Store the updated session + storeSession(txn, storageId, session); return false; } - @Override - public void sendInvitation(GroupId groupId, ContactId contactId, - @Nullable String msg) throws DbException { - - Transaction txn = db.startTransaction(false); - try { - // initialize local state for sharer - S f = getSFactory().get(txn, groupId); - SS localState = initializeSharerState(txn, f, contactId); - - // add invitation message to local state to be available for engine - if (!StringUtils.isNullOrEmpty(msg)) { - int msgLength = StringUtils.toUtf8(msg).length; - if (msgLength > MAX_INVITATION_MESSAGE_LENGTH) - throw new IllegalArgumentException(); - localState.setMessage(msg); - } + private SessionId getSessionId(GroupId shareableId) { + return new SessionId(shareableId.getBytes()); + } - // start engine and process its state update - SharerEngine<I, SS, IRR> engine = - new SharerEngine<I, SS, IRR>(getIFactory(), - getIRRFactory(), clock); - StateUpdate<SS, BaseMessage> update = - engine.onLocalAction(localState, - SharerSessionState.Action.LOCAL_INVITATION); - processSharerStateUpdate(txn, null, update); + @Nullable + private StoredSession getSession(Transaction txn, GroupId contactGroupId, + SessionId sessionId) throws DbException, FormatException { + BdfDictionary query = sessionParser.getSessionQuery(sessionId); + Map<MessageId, BdfDictionary> results = clientHelper + .getMessageMetadataAsDictionary(txn, contactGroupId, query); + if (results.size() > 1) throw new DbException(); + if (results.isEmpty()) return null; + return new StoredSession(results.keySet().iterator().next(), + results.values().iterator().next()); + } - // track message - // TODO handle this properly without engine hacks (#376) - long time = update.toSend.get(0).getTime(); - messageTracker.trackMessage(txn, localState.getContactGroupId(), time, - true); + private Session handleFirstMessage(Transaction txn, Message m, BdfList body, + MessageMetadata meta) throws DbException, FormatException { + GroupId shareableId = meta.getShareableId(); + MessageType type = meta.getMessageType(); + if (type == INVITE) { + Session session = new Session(m.getGroupId(), shareableId); + BdfDictionary d = sessionEncoder.encodeSession(session); + return handleMessage(txn, m, body, meta, d); + } else { + throw new FormatException(); // Invalid first message + } + } - db.commitTransaction(txn); - } catch (FormatException e) { - throw new DbException(); - } finally { - db.endTransaction(txn); + private Session handleMessage(Transaction txn, Message m, BdfList body, + MessageMetadata meta, BdfDictionary d) + throws DbException, FormatException { + MessageType type = meta.getMessageType(); + Session session = sessionParser.parseSession(m.getGroupId(), d); + if (type == INVITE) { + InviteMessage<S> invite = messageParser.parseInviteMessage(m, body); + return engine.onInviteMessage(txn, session, invite); + } else if (type == ACCEPT) { + AcceptMessage accept = messageParser.parseAcceptMessage(m, body); + return engine.onAcceptMessage(txn, session, accept); + } else if (type == DECLINE) { + DeclineMessage decline = messageParser.parseDeclineMessage(m, body); + return engine.onDeclineMessage(txn, session, decline); + } else if (type == LEAVE) { + LeaveMessage leave = messageParser.parseLeaveMessage(m, body); + return engine.onLeaveMessage(txn, session, leave); + } else if (type == ABORT) { + AbortMessage abort = messageParser.parseAbortMessage(m, body); + return engine.onAbortMessage(txn, session, abort); + } else { + throw new AssertionError(); } } - @Override - public void respondToInvitation(S f, Contact c, boolean accept) + private MessageId createStorageId(Transaction txn, GroupId g) throws DbException { + Message m = clientHelper.createMessageForStoringMetadata(g); + db.addLocalMessage(txn, m, new Metadata(), false); + return m.getId(); + } + + private void storeSession(Transaction txn, MessageId storageId, + Session session) throws DbException, FormatException { + BdfDictionary d = sessionEncoder.encodeSession(session); + clientHelper.mergeMessageMetadata(txn, storageId, d); + } + @Override + public void sendInvitation(GroupId shareableId, ContactId contactId, + @Nullable String message, long timestamp) throws DbException { + SessionId sessionId = getSessionId(shareableId); Transaction txn = db.startTransaction(false); try { - // find session state based on shareable - IS localState = getSessionStateForResponse(txn, f, c); - respondToInvitation(txn, localState, accept); + // Look up the session, if there is one + Contact contact = db.getContact(txn, contactId); + GroupId contactGroupId = getContactGroup(contact).getId(); + StoredSession ss = getSession(txn, contactGroupId, sessionId); + // Create or parse the session + Session session; + MessageId storageId; + if (ss == null) { + // This is the first invite - create a new session + session = new Session(contactGroupId, shareableId); + storageId = createStorageId(txn, contactGroupId); + } else { + // We already have a session + session = sessionParser + .parseSession(contactGroupId, ss.bdfSession); + storageId = ss.storageId; + } + // Handle the invite action + session = engine.onInviteAction(txn, session, message, timestamp); + // Store the updated session + storeSession(txn, storageId, session); db.commitTransaction(txn); } catch (FormatException e) { throw new DbException(e); @@ -332,13 +241,29 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS } @Override - public void respondToInvitation(SessionId id, boolean accept) + public void respondToInvitation(S s, Contact c, boolean accept) throws DbException { + respondToInvitation(c.getId(), getSessionId(s.getId()), accept); + } + @Override + public void respondToInvitation(ContactId c, SessionId id, boolean accept) + throws DbException { Transaction txn = db.startTransaction(false); try { - IS localState = (IS) getSessionState(txn, id, true); - respondToInvitation(txn, localState, accept); + // Look up the session + Contact contact = db.getContact(txn, c); + GroupId contactGroupId = getContactGroup(contact).getId(); + StoredSession ss = getSession(txn, contactGroupId, id); + if (ss == null) throw new IllegalArgumentException(); + // Parse the session + Session session = + sessionParser.parseSession(contactGroupId, ss.bdfSession); + // Handle the accept or decline action + if (accept) session = engine.onAcceptAction(txn, session); + 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); @@ -347,144 +272,108 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS } } - private void respondToInvitation(Transaction txn, IS localState, - boolean accept) throws DbException, FormatException { - // define action - InviteeSessionState.Action localAction; - if (accept) { - localAction = InviteeSessionState.Action.LOCAL_ACCEPT; - } else { - localAction = InviteeSessionState.Action.LOCAL_DECLINE; - } - - // start engine and process its state update - InviteeEngine<IS, IR> engine = - new InviteeEngine<IS, IR>(getIRFactory(), clock); - StateUpdate<IS, BaseMessage> update = - engine.onLocalAction(localState, localAction); - processInviteeStateUpdate(txn, null, update); - - // track message - // TODO handle this properly without engine hacks (#376) - long time = update.toSend.get(0).getTime(); - messageTracker.trackMessage(txn, localState.getContactGroupId(), time, true); - } - @Override - public Collection<InvitationMessage> getInvitationMessages( - ContactId contactId) + public Collection<InvitationMessage> getInvitationMessages(ContactId c) throws DbException { - + List<InvitationMessage> messages; Transaction txn = db.startTransaction(true); try { - Contact contact = db.getContact(txn, contactId); - Group group = getContactGroup(contact); - - Collection<InvitationMessage> list = - new ArrayList<InvitationMessage>(); - Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, group.getId()); - for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { - BdfDictionary d = m.getValue(); - long type = d.getLong(TYPE); - if (type == SHARE_MSG_TYPE_LEAVE || - type == SHARE_MSG_TYPE_ABORT) continue; - try { - MessageStatus status = - db.getMessageStatus(txn, contactId, m.getKey()); - SharingSessionState s; - long time = d.getLong(TIME); - boolean local = d.getBoolean(LOCAL); - boolean read = d.getBoolean(MSG_KEY_READ, false); - boolean available = false, canBeOpened = false; - - if (type == SHARE_MSG_TYPE_INVITATION) { - I msg = getIFactory().build(group.getId(), d); - SessionId sessionId = msg.getSessionId(); - s = getSessionState(txn, sessionId, true); - if (!local) { - // figure out whether the shareable is still available - if (!(s instanceof InviteeSessionState)) - continue; - available = ((InviteeSessionState) s).getState() == - AWAIT_LOCAL_RESPONSE; - if (!available) { - canBeOpened = db.containsGroup(txn, - s.getShareableId()); - } - } - InvitationMessage im = - createInvitationRequest(m.getKey(), msg, - contactId, s.getShareableId(), - available, canBeOpened, time, local, - status.isSent(), status.isSeen(), read); - list.add(im); - } else if (type == SHARE_MSG_TYPE_ACCEPT || - type == SHARE_MSG_TYPE_DECLINE) { - boolean accept = type == SHARE_MSG_TYPE_ACCEPT; - BaseMessage msg = BaseMessage - .from(getIFactory(), group.getId(), d); - SessionId sessionId = msg.getSessionId(); - s = getSessionState(txn, sessionId, true); - InvitationMessage im = - createInvitationResponse(m.getKey(), sessionId, - group.getId(), contactId, - s.getShareableId(), accept, time, local, - status.isSent(), status.isSeen(), read); - list.add(im); - } else { - throw new RuntimeException("Unexpected Message Type"); - } - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); + Contact contact = db.getContact(txn, c); + GroupId contactGroupId = getContactGroup(contact).getId(); + BdfDictionary query = messageParser.getMessagesVisibleInUiQuery(); + Map<MessageId, BdfDictionary> results = clientHelper + .getMessageMetadataAsDictionary(txn, contactGroupId, query); + messages = new ArrayList<InvitationMessage>(results.size()); + for (Entry<MessageId, BdfDictionary> e : results.entrySet()) { + MessageId m = e.getKey(); + MessageMetadata meta = + messageParser.parseMetadata(e.getValue()); + MessageStatus status = db.getMessageStatus(txn, c, m); + MessageType type = meta.getMessageType(); + if (type == INVITE) { + messages.add( + parseInvitationRequest(txn, c, m, meta, status)); + } else if (type == ACCEPT) { + messages.add( + parseInvitationResponse(c, contactGroupId, m, meta, + status, true)); + } else if (type == DECLINE) { + messages.add( + parseInvitationResponse(c, contactGroupId, m, meta, + status, false)); } } db.commitTransaction(txn); - return list; } catch (FormatException e) { throw new DbException(e); } finally { db.endTransaction(txn); } + return messages; + } + + private InvitationRequest parseInvitationRequest(Transaction txn, + ContactId c, MessageId m, MessageMetadata meta, + MessageStatus status) throws DbException, FormatException { + // Look up the invite message to get the details of the private group + InviteMessage<S> invite = messageParser.getInviteMessage(txn, m); + boolean canBeOpened = db.containsGroup(txn, invite.getShareableId()); + return invitationFactory + .createInvitationRequest(meta.isLocal(), status.isSent(), + status.isSeen(), meta.isRead(), invite, c, + meta.isAvailableToAnswer(), canBeOpened); + } + + private InvitationResponse parseInvitationResponse(ContactId c, + GroupId contactGroupId, MessageId m, MessageMetadata meta, + MessageStatus status, boolean accept) + throws DbException, FormatException { + return invitationFactory.createInvitationResponse(m, contactGroupId, + meta.getTimestamp(), meta.isLocal(), status.isSent(), + status.isSeen(), meta.isRead(), meta.getShareableId(), c, + accept); } @Override public Collection<SharingInvitationItem> getInvitations() throws DbException { - List<SharingInvitationItem> invitations = + List<SharingInvitationItem> items = new ArrayList<SharingInvitationItem>(); + BdfDictionary query = messageParser.getInvitesAvailableToAnswerQuery(); + Map<S, Collection<Contact>> sharers = + new HashMap<S, Collection<Contact>>(); Transaction txn = db.startTransaction(true); try { - Set<S> shareables = new HashSet<S>(); - Map<GroupId, Collection<Contact>> newSharers = - new HashMap<GroupId, Collection<Contact>>(); - Collection<Contact> contacts = db.getContacts(txn); - // get invitations from each contact - for (Contact contact : contacts) { - Collection<S> newShareables = getInvited(txn, contact); - shareables.addAll(newShareables); - for (S s : newShareables) { - if (newSharers.containsKey(s.getId())) { - newSharers.get(s.getId()).add(contact); + for (Contact c : db.getContacts(txn)) { + GroupId contactGroupId = getContactGroup(c).getId(); + Map<MessageId, BdfDictionary> results = + clientHelper.getMessageMetadataAsDictionary(txn, + contactGroupId, query); + for (MessageId m : results.keySet()) { + InviteMessage<S> invite = + messageParser.getInviteMessage(txn, m); + S s = invite.getShareable(); + if (sharers.containsKey(s)) { + sharers.get(s).add(c); } else { - Collection<Contact> c = new ArrayList<Contact>(); - c.add(contact); - newSharers.put(s.getId(), c); + Collection<Contact> contacts = new ArrayList<Contact>(); + contacts.add(c); + sharers.put(s, contacts); } } } - // construct InvitationItem objects - for (S s : shareables) { - Collection<Contact> newS = newSharers.get(s.getId()); + // construct the invitation items + for (Entry<S, Collection<Contact>> e : sharers.entrySet()) { + S s = e.getKey(); + Collection<Contact> contacts = e.getValue(); boolean subscribed = db.containsGroup(txn, s.getId()); SharingInvitationItem invitation = - new SharingInvitationItem(s, subscribed, newS); - invitations.add(invitation); + new SharingInvitationItem(s, subscribed, contacts); + items.add(invitation); } db.commitTransaction(txn); - return invitations; + return items; } catch (FormatException e) { throw new DbException(e); } finally { @@ -492,560 +381,75 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS } } - private Collection<S> getInvited(Transaction txn, Contact contact) - throws DbException, FormatException { - - // query for all external invitations - BdfDictionary query = BdfDictionary.of( - new BdfEntry(TYPE, SHARE_MSG_TYPE_INVITATION), - new BdfEntry(LOCAL, false) - ); - Group group = getContactGroup(contact); - - Set<S> invited = new HashSet<S>(); - Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, group.getId(), query); - for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { - BdfDictionary d = m.getValue(); - try { - I msg = getIFactory().build(group.getId(), d); - IS iss = (IS) getSessionState(txn, msg.getSessionId(), true); - // get and add the shareable if the invitation is unanswered - if (iss.getState().equals(AWAIT_LOCAL_RESPONSE)) { - S s = getSFactory().parse(iss); - invited.add(s); - } - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - } - } - return invited; - } - @Override public Collection<Contact> getSharedWith(GroupId g) throws DbException { + // TODO report also pending invitations + Collection<Contact> contacts = new ArrayList<Contact>(); + Transaction txn = db.startTransaction(true); try { - List<Contact> shared = new ArrayList<Contact>(); - Transaction txn = db.startTransaction(true); - try { - for (Contact c : db.getContacts(txn)) { - GroupId contactGroup = getContactGroup(c).getId(); - if (listContains(txn, contactGroup, g, SHARED_BY_US)) - shared.add(c); - else if (listContains(txn, contactGroup, g, SHARED_WITH_US)) - shared.add(c); - } - db.commitTransaction(txn); - } finally { - db.endTransaction(txn); + for (Contact c : db.getContacts(txn)) { + if (db.getGroupVisibility(txn, c.getId(), g) == SHARED) + contacts.add(c); } - return shared; - } catch (FormatException e) { - throw new DbException(e); + db.commitTransaction(txn); + } finally { + db.endTransaction(txn); } + return contacts; } @Override public boolean canBeShared(GroupId g, Contact c) throws DbException { - boolean canBeShared; + GroupId contactGroupId = getContactGroup(c).getId(); + SessionId sessionId = getSessionId(g); Transaction txn = db.startTransaction(true); try { - canBeShared = canBeShared(txn, g, c); + StoredSession ss = getSession(txn, contactGroupId, sessionId); db.commitTransaction(txn); + // If there's no session, we can share the group with the contact + if (ss == null) return true; + // If the session's in the right state, the contact can be invited + Session session = + sessionParser.parseSession(contactGroupId, ss.bdfSession); + return session.getState().canInvite(); + } catch (FormatException e) { + throw new DbException(e); } finally { db.endTransaction(txn); } - return canBeShared; } - protected boolean canBeShared(Transaction txn, GroupId g, Contact c) + protected void removingShareable(Transaction txn, S shareable) throws DbException { - - try { - GroupId contactGroup = getContactGroup(c).getId(); - return !listContains(txn, contactGroup, g, SHARED_BY_US) && - !listContains(txn, contactGroup, g, SHARED_WITH_US) && - !listContains(txn, contactGroup, g, TO_BE_SHARED_BY_US); - } catch (FormatException e) { - throw new DbException(e); - } - } - - void removingShareable(Transaction txn, S f) throws DbException { + SessionId sessionId = getSessionId(shareable.getId()); + // If we have any sessions in progress, tell the contacts we're leaving try { for (Contact c : db.getContacts(txn)) { - GroupId g = getContactGroup(c).getId(); - if (removeFromList(txn, g, TO_BE_SHARED_BY_US, f)) { - leaveShareable(txn, c.getId(), f); - } - if (removeFromList(txn, g, SHARED_BY_US, f)) { - leaveShareable(txn, c.getId(), f); - } - if (removeFromList(txn, g, SHARED_WITH_US, f)) { - leaveShareable(txn, c.getId(), f); - } + // Look up the session for the contact, if there is one + GroupId contactGroupId = getContactGroup(c).getId(); + StoredSession ss = getSession(txn, contactGroupId, sessionId); + if (ss == null) continue; // No session for this contact + // Let the engine perform a LEAVE action + Session session = sessionParser + .parseSession(contactGroupId, ss.bdfSession); + session = engine.onLeaveAction(txn, session); + // Store the updated session + storeSession(txn, ss.storageId, session); } - } catch (IOException e) { + } catch (FormatException e) { throw new DbException(e); } } - private void checkForRaceCondition(Transaction txn, S f, Contact c) - throws FormatException, DbException { + private static class StoredSession { - GroupId contactGroup = getContactGroup(c).getId(); - if (!listContains(txn, contactGroup, f.getId(), TO_BE_SHARED_BY_US)) - // no race-condition, this invitation is invalid - throw new FormatException(); + private final MessageId storageId; + private final BdfDictionary bdfSession; - // we have an invitation race condition - LocalAuthor author = db.getLocalAuthor(txn, c.getLocalAuthorId()); - Bytes ourKey = new Bytes(author.getPublicKey()); - Bytes theirKey = new Bytes(c.getAuthor().getPublicKey()); - - // determine which invitation takes precedence - boolean alice = ourKey.compareTo(theirKey) < 0; - - if (alice) { - // our own invitation takes precedence, so just delete Bob's - LOG.info( - "Invitation race-condition: We are Alice deleting Bob's invitation."); - throw new FormatException(); - } else { - // we are Bob, so we need to "take back" our own invitation - LOG.info( - "Invitation race-condition: We are Bob taking back our invitation."); - SharingSessionState state = - getSessionStateForLeaving(txn, f, c.getId()); - if (state instanceof SharerSessionState) { - //SharerEngine engine = new SharerEngine(); - //processSharerStateUpdate(txn, null, - // engine.onLocalAction((SharerSessionState) state, - // Action.LOCAL_LEAVE)); - - // simply remove from list instead of involving engine - removeFromList(txn, contactGroup, TO_BE_SHARED_BY_US, f); - // TODO here we could also remove the old session state - // and invitation message - } + private StoredSession(MessageId storageId, BdfDictionary bdfSession) { + this.storageId = storageId; + this.bdfSession = bdfSession; } - - } - - private SS initializeSharerState(Transaction txn, S f, - ContactId contactId) throws FormatException, DbException { - - Contact c = db.getContact(txn, contactId); - Group group = getContactGroup(c); - - // create local message to keep engine state - long now = clock.currentTimeMillis(); - Bytes salt = new Bytes(new byte[SHARING_SALT_LENGTH]); - random.nextBytes(salt.getBytes()); - Message m = clientHelper.createMessage(localGroup.getId(), now, - BdfList.of(salt)); - SessionId sessionId = new SessionId(m.getId().getBytes()); - - SS s = getSSFactory().build(sessionId, m.getId(), group.getId(), - SharerSessionState.State.PREPARE_INVITATION, contactId, f); - - // save local state to database - BdfDictionary d = s.toBdfDictionary(); - clientHelper.addLocalMessage(txn, m, d, false); - - return s; - } - - private IS initializeInviteeState(Transaction txn, - ContactId contactId, I msg, MessageId id) - throws FormatException, DbException { - - Contact c = db.getContact(txn, contactId); - Group group = getContactGroup(c); - S f = getSFactory().parse(msg); - - // create local message to keep engine state - long now = clock.currentTimeMillis(); - Bytes mSalt = new Bytes(new byte[SHARING_SALT_LENGTH]); - random.nextBytes(mSalt.getBytes()); - Message m = clientHelper.createMessage(localGroup.getId(), now, - BdfList.of(mSalt)); - - IS s = getISFactory() - .build(msg.getSessionId(), m.getId(), group.getId(), - InviteeSessionState.State.AWAIT_INVITATION, contactId, - f, id); - - // save local state to database - BdfDictionary d = s.toBdfDictionary(); - clientHelper.addLocalMessage(txn, m, d, false); - - return s; - } - - private SharingSessionState getSessionState(Transaction txn, - SessionId sessionId, boolean warn) - throws DbException, FormatException { - - try { - return getSessionStateForSharer(txn, sessionId); - } catch (NoSuchMessageException e) { - // State not found directly, so query for state for invitee - BdfDictionary query = BdfDictionary.of( - new BdfEntry(SESSION_ID, sessionId) - ); - - Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, localGroup.getId(), - query); - - if (map.size() > 1 && LOG.isLoggable(WARNING)) { - LOG.warning( - "More than one session state found for message with session ID " + - Arrays.hashCode(sessionId.getBytes())); - } - if (map.isEmpty()) { - if (warn && LOG.isLoggable(WARNING)) { - LOG.warning( - "No session state found for message with session ID " + - Arrays.hashCode(sessionId.getBytes())); - } - throw new FormatException(); - } - return SharingSessionState - .fromBdfDictionary(getISFactory(), getSSFactory(), - map.values().iterator().next()); - } - } - - private SS getSessionStateForSharer(Transaction txn, - SessionId sessionId) - throws DbException, FormatException { - - // we should be able to get the sharer state directly from sessionId - MessageId storageId = new MessageId(sessionId.getBytes()); - BdfDictionary d = - clientHelper.getMessageMetadataAsDictionary(txn, storageId); - - if (!d.getBoolean(IS_SHARER)) throw new FormatException(); - - return (SS) SharingSessionState - .fromBdfDictionary(getISFactory(), getSSFactory(), d); - } - - private IS getSessionStateForResponse(Transaction txn, - S f, Contact c) throws DbException, FormatException { - - // query for invitee states for that shareable in state await response - BdfDictionary query = BdfDictionary.of( - new BdfEntry(IS_SHARER, false), - new BdfEntry(CONTACT_ID, c.getId().getInt()), - new BdfEntry(SHAREABLE_ID, f.getId()), - new BdfEntry(STATE, AWAIT_LOCAL_RESPONSE.getValue()) - ); - - Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, localGroup.getId(), query); - - if (map.size() > 1 && LOG.isLoggable(WARNING)) { - LOG.warning( - "More than one session state found for shareable with ID " + - Arrays.hashCode(f.getId().getBytes()) + - " in state AWAIT_LOCAL_RESPONSE for contact " + - c.getAuthor().getName()); - } - if (map.isEmpty()) { - if (LOG.isLoggable(WARNING)) { - LOG.warning( - "No session state found for shareable with ID " + - Arrays.hashCode(f.getId().getBytes()) + - " in state AWAIT_LOCAL_RESPONSE"); - } - throw new DbException(); - } - return (IS) SharingSessionState - .fromBdfDictionary(getISFactory(), getSSFactory(), - map.values().iterator().next()); - } - - private SharingSessionState getSessionStateForLeaving(Transaction txn, - S f, ContactId c) throws DbException, FormatException { - - BdfDictionary query = BdfDictionary.of( - new BdfEntry(CONTACT_ID, c.getInt()), - new BdfEntry(SHAREABLE_ID, f.getId()) - ); - Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, localGroup.getId(), query); - for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { - BdfDictionary d = m.getValue(); - try { - SharingSessionState s = SharingSessionState - .fromBdfDictionary(getISFactory(), getSSFactory(), d); - - // check that a shareable get be left in current session - if (s instanceof SharerSessionState) { - SharerSessionState state = (SharerSessionState) s; - SharerSessionState.State nextState = - state.getState() - .next(SharerSessionState.Action.LOCAL_LEAVE); - if (nextState != SharerSessionState.State.ERROR) { - return state; - } - } else { - InviteeSessionState state = (InviteeSessionState) s; - InviteeSessionState.State nextState = state.getState() - .next(InviteeSessionState.Action.LOCAL_LEAVE); - if (nextState != InviteeSessionState.State.ERROR) { - return state; - } - } - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - } - } - throw new FormatException(); - } - - private void processStateUpdate(Transaction txn, - @Nullable MessageId messageId, - StateUpdate<SharingSessionState, BaseMessage> result, S f) - throws DbException, FormatException { - - // perform actions based on new local state - performTasks(txn, result.localState, f); - - // save new local state - MessageId storageId = result.localState.getStorageId(); - clientHelper.mergeMessageMetadata(txn, storageId, - result.localState.toBdfDictionary()); - - // send messages - for (BaseMessage msg : result.toSend) { - sendMessage(txn, msg); - } - - // broadcast events - for (Event event : result.toBroadcast) { - txn.attach(event); - } - - // delete message - if (result.deleteMessage && messageId != null) { - if (LOG.isLoggable(INFO)) { - LOG.info("Deleting message with id " + messageId.hashCode()); - } - db.deleteMessage(txn, messageId); - db.deleteMessageMetadata(txn, messageId); - } - } - - private void processSharerStateUpdate(Transaction txn, - @Nullable MessageId messageId, StateUpdate<SS, BaseMessage> result) - throws DbException, FormatException { - - StateUpdate<SharingSessionState, BaseMessage> r = - new StateUpdate<SharingSessionState, BaseMessage>( - result.deleteMessage, result.deleteState, - result.localState, result.toSend, result.toBroadcast); - - // get shareable for later - S f = getSFactory().parse(result.localState); - - processStateUpdate(txn, messageId, r, f); - } - - private void processInviteeStateUpdate(Transaction txn, - @Nullable MessageId messageId, StateUpdate<IS, BaseMessage> result) - throws DbException, FormatException { - - StateUpdate<SharingSessionState, BaseMessage> r = - new StateUpdate<SharingSessionState, BaseMessage>( - result.deleteMessage, result.deleteState, - result.localState, result.toSend, result.toBroadcast); - - // get shareable for later - S f = getSFactory().parse(result.localState); - - processStateUpdate(txn, messageId, r, f); - } - - private void performTasks(Transaction txn, SharingSessionState localState, - S f) throws FormatException, DbException { - - if (localState.getTask() == -1) return; - - // remember task and remove it from localState - long task = localState.getTask(); - localState.setTask(-1); - - // get group ID for later - GroupId groupId = localState.getContactGroupId(); - // get contact ID for later - ContactId contactId = localState.getContactId(); - - // perform tasks - if (task == TASK_ADD_SHAREABLE_TO_LIST_SHARED_WITH_US) { - addToList(txn, groupId, SHARED_WITH_US, f); - } else if (task == TASK_REMOVE_SHAREABLE_FROM_LIST_SHARED_WITH_US) { - removeFromList(txn, groupId, SHARED_WITH_US, f); - } else if (task == TASK_ADD_SHARED_SHAREABLE) { - // TODO we might want to call the add() method of the respective - // manager here, because blogs add a description for example - db.addGroup(txn, f.getGroup()); - db.setGroupVisibility(txn, contactId, f.getId(), SHARED); - } else if (task == TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US) { - addToList(txn, groupId, TO_BE_SHARED_BY_US, f); - } else if (task == TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US) { - removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f); - } else if (task == TASK_SHARE_SHAREABLE) { - db.setGroupVisibility(txn, contactId, f.getId(), SHARED); - removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f); - addToList(txn, groupId, SHARED_BY_US, f); - } else if (task == TASK_UNSHARE_SHAREABLE_SHARED_BY_US) { - db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE); - removeFromList(txn, groupId, SHARED_BY_US, f); - // broadcast event informing UI that contact has left the group - ContactLeftShareableEvent - e = new ContactLeftShareableEvent(f.getId(), contactId); - txn.attach(e); - } else if (task == TASK_UNSHARE_SHAREABLE_SHARED_WITH_US) { - db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE); - removeFromList(txn, groupId, SHARED_WITH_US, f); - // broadcast event informing UI that contact has left the group - ContactLeftShareableEvent - e = new ContactLeftShareableEvent(f.getId(), contactId); - txn.attach(e); - } - } - - private void sendMessage(Transaction txn, BaseMessage m) - throws FormatException, DbException { - - byte[] body = clientHelper.toByteArray(m.toBdfList()); - Group group = db.getGroup(txn, m.getGroupId()); - - // add message itself as metadata - BdfDictionary d = m.toBdfDictionary(); - d.put(LOCAL, true); - d.put(TIME, m.getTime()); - Metadata meta = metadataEncoder.encode(d); - - messageQueueManager - .sendMessage(txn, group, m.getTime(), body, meta); - } - - @Override - public Group getContactGroup(Contact c) { - return contactGroupFactory.createContactGroup(getClientId(), c); - } - - private ContactId getContactId(Transaction txn, GroupId contactGroupId) - throws DbException, FormatException { - BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn, - contactGroupId); - return new ContactId(meta.getLong(CONTACT_ID).intValue()); - } - - private void leaveShareable(Transaction txn, ContactId c, S f) - throws DbException, FormatException { - - SharingSessionState state = getSessionStateForLeaving(txn, f, c); - if (state instanceof SharerSessionState) { - SharerSessionState.Action action = - SharerSessionState.Action.LOCAL_LEAVE; - SharerEngine<I, SS, IRR> engine = - new SharerEngine<I, SS, IRR>(getIFactory(), - getIRRFactory(), clock); - processSharerStateUpdate(txn, null, - engine.onLocalAction((SS) state, action)); - } else { - InviteeSessionState.Action action = - InviteeSessionState.Action.LOCAL_LEAVE; - InviteeEngine<IS, IR> engine = - new InviteeEngine<IS, IR>(getIRFactory(), clock); - processInviteeStateUpdate(txn, null, - engine.onLocalAction((IS) state, action)); - } - } - - private boolean listContains(Transaction txn, GroupId contactGroup, - GroupId shareable, String key) throws DbException, FormatException { - - List<S> list = getShareableList(txn, contactGroup, key); - for (S f : list) { - if (f.getId().equals(shareable)) return true; - } - return false; - } - - private boolean addToList(Transaction txn, GroupId groupId, String key, - S f) throws DbException, FormatException { - - List<S> shareables = getShareableList(txn, groupId, key); - if (shareables.contains(f)) return false; - shareables.add(f); - storeShareableList(txn, groupId, key, shareables); - return true; - } - - private boolean removeFromList(Transaction txn, GroupId groupId, String key, - S f) throws DbException, FormatException { - - List<S> shareables = getShareableList(txn, groupId, key); - if (shareables.remove(f)) { - storeShareableList(txn, groupId, key, shareables); - return true; - } - return false; - } - - private List<S> getShareableList(Transaction txn, GroupId groupId, - String key) throws DbException, FormatException { - - BdfDictionary metadata = - clientHelper.getGroupMetadataAsDictionary(txn, groupId); - BdfList list = metadata.getList(key); - - return parseShareableList(list); - } - - private void storeShareableList(Transaction txn, GroupId groupId, - String key, - List<S> shareables) throws DbException, FormatException { - - BdfList list = encodeShareableList(shareables); - BdfDictionary metadata = BdfDictionary.of( - new BdfEntry(key, list) - ); - clientHelper.mergeGroupMetadata(txn, groupId, metadata); - } - - private BdfList encodeShareableList(List<S> shareables) { - BdfList shareableList = new BdfList(); - for (S f : shareables) - shareableList.add(getSFactory().encode(f)); - return shareableList; - } - - private List<S> parseShareableList(BdfList list) throws FormatException { - List<S> shareables = new ArrayList<S>(list.size()); - for (int i = 0; i < list.size(); i++) { - BdfList shareable = list.getList(i); - shareables.add(getSFactory().parse(shareable)); - } - return shareables; - } - - private void deleteMessage(Transaction txn, MessageId messageId) - throws DbException { - - if (LOG.isLoggable(INFO)) - LOG.info("Deleting message with ID: " + messageId.hashCode()); - - db.deleteMessage(txn, messageId); - db.deleteMessageMetadata(txn, messageId); } } diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingMessage.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..5d18579e050e284a82869f78c81d77876c4169b0 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingMessage.java @@ -0,0 +1,50 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +abstract class SharingMessage { + + private final MessageId id; + private final GroupId contactGroupId, shareableId; + private final long timestamp; + @Nullable + private final MessageId previousMessageId; + + SharingMessage(MessageId id, GroupId contactGroupId, GroupId shareableId, + long timestamp, @Nullable MessageId previousMessageId) { + this.id = id; + this.previousMessageId = previousMessageId; + this.contactGroupId = contactGroupId; + this.shareableId = shareableId; + this.timestamp = timestamp; + } + + MessageId getId() { + return id; + } + + GroupId getContactGroupId() { + return contactGroupId; + } + + GroupId getShareableId() { + return shareableId; + } + + long getTimestamp() { + return timestamp; + } + + @Nullable + public MessageId getPreviousMessageId() { + return previousMessageId; + } + +} 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 f56e6b8ca88b99279002853e3508a9390ac8673e..e4a24c1b73b1379b6eca6b36ef72e0a8c2c0570b 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 @@ -4,10 +4,13 @@ import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.data.MetadataEncoder; import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.sync.ValidationManager; import org.briarproject.bramble.api.system.Clock; import org.briarproject.briar.api.blog.BlogManager; import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.client.MessageQueueManager; +import org.briarproject.briar.api.forum.Forum; +import org.briarproject.briar.api.forum.ForumFactory; import org.briarproject.briar.api.forum.ForumManager; import org.briarproject.briar.api.forum.ForumSharingManager; import org.briarproject.briar.api.messaging.ConversationManager; @@ -68,14 +71,15 @@ public class SharingModule { @Provides @Singleton ForumSharingValidator provideForumSharingValidator( - MessageQueueManager messageQueueManager, ClientHelper clientHelper, - MetadataEncoder metadataEncoder, Clock clock) { - + ValidationManager validationManager, MessageEncoder messageEncoder, + ClientHelper clientHelper, MetadataEncoder metadataEncoder, + Clock clock, ForumFactory forumFactory) { ForumSharingValidator validator = - new ForumSharingValidator(clientHelper, metadataEncoder, clock); - messageQueueManager.registerMessageValidator( - ForumSharingManager.CLIENT_ID, validator); - + new ForumSharingValidator(messageEncoder, clientHelper, + metadataEncoder, clock, forumFactory); + validationManager + .registerMessageValidator(ForumSharingManager.CLIENT_ID, + validator); return validator; } @@ -83,14 +87,14 @@ public class SharingModule { @Singleton ForumSharingManager provideForumSharingManager( LifecycleManager lifecycleManager, ContactManager contactManager, - MessageQueueManager messageQueueManager, + ValidationManager validationManager, ConversationManager conversationManager, ForumManager forumManager, ForumSharingManagerImpl forumSharingManager) { lifecycleManager.registerClient(forumSharingManager); contactManager.registerAddContactHook(forumSharingManager); contactManager.registerRemoveContactHook(forumSharingManager); - messageQueueManager.registerIncomingMessageHook( + validationManager.registerIncomingMessageHook( ForumSharingManager.CLIENT_ID, forumSharingManager); conversationManager.registerConversationClient(forumSharingManager); forumManager.registerRemoveForumHook(forumSharingManager); @@ -98,4 +102,37 @@ public class SharingModule { return forumSharingManager; } + @Provides + MessageEncoder provideMessageEncoder(MessageEncoderImpl messageEncoder) { + return messageEncoder; + } + + @Provides + MessageParser<Forum> provideForumMessageParser( + ForumMessageParserImpl forumMessageParser) { + return forumMessageParser; + } + + @Provides + SessionEncoder provideSessionEncoder(SessionEncoderImpl sessionEncoder) { + return sessionEncoder; + } + + @Provides + SessionParser provideSessionParser(SessionParserImpl sessionParser) { + return sessionParser; + } + + @Provides + ProtocolEngine<Forum> provideForumProtocolEngine( + ForumProtocolEngineImpl forumProtocolEngine) { + return forumProtocolEngine; + } + + @Provides + InvitationFactory<Forum> provideForumInvitationFactory( + ForumInvitationFactoryImpl forumInvitationFactory) { + return forumInvitationFactory; + } + } diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingSessionState.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingSessionState.java index 791ca044618283be64260b81d10330f0a2f30c91..77ddbd52c4a5ab780a9f76bbae030abf7b05bcfb 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingSessionState.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingSessionState.java @@ -18,6 +18,7 @@ import static org.briarproject.briar.api.sharing.SharingConstants.SHAREABLE_ID; import static org.briarproject.briar.api.sharing.SharingConstants.STATE; import static org.briarproject.briar.api.sharing.SharingConstants.STORAGE_ID; +@Deprecated @NotThreadSafe @NotNullByDefault abstract class SharingSessionState { diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingValidator.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..e8f9287f8f43162acd224d6ce0dc43dcc78c7fb3 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingValidator.java @@ -0,0 +1,101 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.UniqueId; +import org.briarproject.bramble.api.client.BdfMessageContext; +import org.briarproject.bramble.api.client.BdfMessageValidator; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.data.MetadataEncoder; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; + +import java.util.Collections; + +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.bramble.util.ValidationUtils.checkLength; +import static org.briarproject.bramble.util.ValidationUtils.checkSize; +import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH; +import static org.briarproject.briar.sharing.MessageType.INVITE; + +@Immutable +@NotNullByDefault +abstract class SharingValidator extends BdfMessageValidator { + + private final MessageEncoder messageEncoder; + + SharingValidator(MessageEncoder messageEncoder, ClientHelper clientHelper, + MetadataEncoder metadataEncoder, Clock clock) { + super(clientHelper, metadataEncoder, clock); + this.messageEncoder = messageEncoder; + } + + @Override + protected BdfMessageContext validateMessage(Message m, Group g, + BdfList body) throws FormatException { + MessageType type = MessageType.fromValue(body.getLong(0).intValue()); + switch (type) { + case INVITE: + return validateInviteMessage(m, body); + case ACCEPT: + case DECLINE: + case LEAVE: + case ABORT: + return validateNonInviteMessage(type, m, body); + default: + throw new FormatException(); + } + } + + private BdfMessageContext validateInviteMessage(Message m, BdfList body) + throws FormatException { + checkSize(body, 4); + byte[] previousMessageId = body.getOptionalRaw(1); + checkLength(previousMessageId, UniqueId.LENGTH); + BdfList descriptor = body.getList(2); + GroupId shareableId = validateDescriptor(descriptor); + String msg = body.getOptionalString(3); + checkLength(msg, 1, MAX_INVITATION_MESSAGE_LENGTH); + + BdfDictionary meta = messageEncoder + .encodeMetadata(INVITE, shareableId, m.getTimestamp(), false, + false, false, false); + if (previousMessageId == null) { + return new BdfMessageContext(meta); + } else { + MessageId dependency = new MessageId(previousMessageId); + return new BdfMessageContext(meta, + Collections.singletonList(dependency)); + } + } + + protected abstract GroupId validateDescriptor(BdfList descriptor) + throws FormatException; + + private BdfMessageContext validateNonInviteMessage(MessageType type, + Message m, BdfList body) throws FormatException { + checkSize(body, 3); + byte[] shareableId = body.getRaw(1); + checkLength(shareableId, UniqueId.LENGTH); + byte[] previousMessageId = body.getOptionalRaw(2); + checkLength(previousMessageId, UniqueId.LENGTH); + + BdfDictionary meta = messageEncoder + .encodeMetadata(type, new GroupId(shareableId), + m.getTimestamp(), false, false, false, false); + if (previousMessageId == null) { + return new BdfMessageContext(meta); + } else { + MessageId dependency = new MessageId(previousMessageId); + return new BdfMessageContext(meta, + Collections.singletonList(dependency)); + } + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/State.java b/briar-core/src/main/java/org/briarproject/briar/sharing/State.java new file mode 100644 index 0000000000000000000000000000000000000000..e4ba53501f7da275fb51c67b0a53418f7f718429 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/State.java @@ -0,0 +1,34 @@ +package org.briarproject.briar.sharing; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +enum State { + + START(0), LOCAL_INVITED(1), REMOTE_INVITED(2), SHARING(3), LOCAL_LEFT(4), + REMOTE_LEFT(5), REMOTE_HANGING(6), ERROR(7); + + private final int value; + + State(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public boolean canInvite() { + return this == START || this == REMOTE_LEFT; + } + + static State fromValue(int value) throws FormatException { + for (State s : values()) if (s.value == value) return s; + throw new FormatException(); + } + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java b/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java index 9ca8805f20819998ad4df22c41a798315d10e19e..30567fdf225431c37ef54d27fc1908c2d2e7c9e1 100644 --- a/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java @@ -45,7 +45,8 @@ public class ForumManagerTest forum0 = forumManager0.addForum("Test Forum"); groupId0 = forum0.getId(); // share forum - forumSharingManager0.sendInvitation(groupId0, contactId1From0, null); + forumSharingManager0.sendInvitation(groupId0, contactId1From0, null, + clock.currentTimeMillis()); sync0To1(1, true); forumSharingManager1.respondToInvitation(forum0, contact0From1, true); sync1To0(1, true); @@ -189,7 +190,8 @@ public class ForumManagerTest // share a second forum Forum forum1 = forumManager0.addForum("Test Forum1"); GroupId g1 = forum1.getId(); - forumSharingManager0.sendInvitation(g1, contactId1From0, null); + forumSharingManager0.sendInvitation(g1, contactId1From0, null, + clock.currentTimeMillis()); sync0To1(1, true); forumSharingManager1.respondToInvitation(forum1, contact0From1, true); sync1To0(1, true); diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java index 7431ffe483f160022bbf03d1ea51507a3e8b5c45..fe4142331256902e70f9f581ff1098aa98f30bc0 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationIntegrationTest.java @@ -12,6 +12,7 @@ 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.api.sharing.InvitationMessage; +import org.briarproject.briar.api.sharing.InvitationResponse; import org.briarproject.briar.test.BriarIntegrationTest; import org.briarproject.briar.test.BriarIntegrationTestComponent; import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent; @@ -97,10 +98,10 @@ public class GroupInvitationIntegrationTest GroupInvitationRequest request = (GroupInvitationRequest) messages.iterator().next(); assertEquals(msg, request.getMessage()); - assertEquals(author0, request.getCreator()); + assertEquals(author0, request.getShareable().getCreator()); assertEquals(timestamp, request.getTimestamp()); assertEquals(contactId0From1, request.getContactId()); - assertEquals(privateGroup0.getName(), request.getGroupName()); + assertEquals(privateGroup0.getName(), request.getShareable().getName()); assertFalse(request.isLocal()); assertFalse(request.isRead()); } @@ -123,7 +124,7 @@ public class GroupInvitationIntegrationTest for (InvitationMessage m : messages) { if (m instanceof GroupInvitationResponse) { foundResponse = true; - GroupInvitationResponse response = (GroupInvitationResponse) m; + InvitationResponse response = (GroupInvitationResponse) m; assertEquals(contactId0From1, response.getContactId()); assertTrue(response.isLocal()); assertFalse(response.wasAccepted()); @@ -140,7 +141,7 @@ public class GroupInvitationIntegrationTest for (InvitationMessage m : messages) { if (m instanceof GroupInvitationResponse) { foundResponse = true; - GroupInvitationResponse response = (GroupInvitationResponse) m; + InvitationResponse response = (GroupInvitationResponse) m; assertEquals(contactId0From1, response.getContactId()); assertFalse(response.isLocal()); assertFalse(response.wasAccepted()); @@ -172,7 +173,7 @@ public class GroupInvitationIntegrationTest for (InvitationMessage m : messages) { if (m instanceof GroupInvitationResponse) { foundResponse = true; - GroupInvitationResponse response = (GroupInvitationResponse) m; + InvitationResponse response = (GroupInvitationResponse) m; assertTrue(response.wasAccepted()); } } @@ -187,7 +188,7 @@ public class GroupInvitationIntegrationTest for (InvitationMessage m : messages) { if (m instanceof GroupInvitationResponse) { foundResponse = true; - GroupInvitationResponse response = (GroupInvitationResponse) m; + InvitationResponse response = (GroupInvitationResponse) m; assertTrue(response.wasAccepted()); } } 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 620775b541444874f9276567a926a8a682f88354..37c7b76e321a1e89a0d9ceb7c105499465dd5d31 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 @@ -90,8 +90,6 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { private final GroupInvitationManagerImpl groupInvitationManager; - private final Group localGroup = - new Group(new GroupId(getRandomId()), CLIENT_ID, getRandomBytes(5)); private final Transaction txn = new Transaction(null, false); private final ContactId contactId = new ContactId(0); private final Author author = @@ -141,8 +139,6 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { will(returnValue(inviteeEngine)); oneOf(engineFactory).createPeerEngine(); will(returnValue(peerEngine)); - oneOf(contactGroupFactory).createLocalGroup(CLIENT_ID); - will(returnValue(localGroup)); }}); MetadataParser metadataParser = context.mock(MetadataParser.class); MessageTracker messageTracker = context.mock(MessageTracker.class); @@ -156,7 +152,6 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { @Test public void testCreateLocalState() throws Exception { context.checking(new Expectations() {{ - oneOf(db).addGroup(txn, localGroup); oneOf(db).getContacts(txn); will(returnValue(Collections.singletonList(contact))); }}); @@ -651,6 +646,9 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { new InviteMessage(message.getId(), contactGroup.getId(), privateGroup.getId(), time1, "name", author, new byte[0], null, new byte[0]); + final PrivateGroup pg = + new PrivateGroup(privateGroup, invite.getGroupName(), + invite.getCreator(), invite.getSalt()); context.checking(new Expectations() {{ oneOf(db).startTransaction(true); @@ -668,12 +666,11 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { oneOf(messageParser).parseMetadata(meta); will(returnValue(messageMetadata1)); oneOf(db).getMessageStatus(txn, contactId, message.getId()); - oneOf(clientHelper).getMessage(txn, message.getId()); - will(returnValue(message)); - oneOf(clientHelper).toList(message); - will(returnValue(body)); - oneOf(messageParser).parseInviteMessage(message, body); + oneOf(messageParser).getInviteMessage(txn, message.getId()); will(returnValue(invite)); + oneOf(privateGroupFactory).createPrivateGroup(invite.getGroupName(), + invite.getCreator(), invite.getSalt()); + will(returnValue(pg)); oneOf(db).containsGroup(txn, privateGroup.getId()); will(returnValue(true)); // second message @@ -742,21 +739,13 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { contactGroup.getId(), query); will(returnValue(results)); // message 1 - oneOf(clientHelper).getMessage(txn, message.getId()); - will(returnValue(message)); - oneOf(clientHelper).toList(message); - will(returnValue(body)); - oneOf(messageParser).parseInviteMessage(message, body); + oneOf(messageParser).getInviteMessage(txn, message.getId()); will(returnValue(inviteMessage1)); oneOf(privateGroupFactory).createPrivateGroup(groupName, author, salt); will(returnValue(pg)); // message 2 - oneOf(clientHelper).getMessage(txn, messageId2); - will(returnValue(message2)); - oneOf(clientHelper).toList(message2); - will(returnValue(body2)); - oneOf(messageParser).parseInviteMessage(message2, body2); + oneOf(messageParser).getInviteMessage(txn, messageId2); will(returnValue(inviteMessage2)); oneOf(privateGroupFactory).createPrivateGroup(groupName, author, salt); 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 2e4c64ddb195b12e99cd89f3793cc0b417b61e1c..6e427277087c8550dfedfc14031106dd2b3f2ea4 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 @@ -144,11 +144,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest { expectSendJoinMessage(properJoinMessage, true); context.checking(new Expectations() {{ oneOf(messageTracker).trackOutgoingMessage(txn, message); - oneOf(clientHelper).getMessage(txn, lastRemoteMessageId); - will(returnValue(inviteMsg)); - oneOf(clientHelper).toList(inviteMsg); - will(returnValue(inviteList)); - oneOf(messageParser).parseInviteMessage(inviteMsg, inviteList); + oneOf(messageParser).getInviteMessage(txn, lastRemoteMessageId); will(returnValue(inviteMessage)); oneOf(privateGroupFactory) .createPrivateGroup(inviteMessage.getGroupName(), diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingIntegrationTest.java index e5e65f94f9256cc30cf72e86b1bdf548235b1638..67b1a37614252402e66bf4488521b73b7187094a 100644 --- a/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/sharing/BlogSharingIntegrationTest.java @@ -107,7 +107,8 @@ public class BlogSharingIntegrationTest // create invitation blogSharingManager0 - .sendInvitation(blog1.getId(), contactId1From0, "Hi!"); + .sendInvitation(blog1.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync invitation sync0To1(1, false); @@ -122,7 +123,8 @@ public class BlogSharingIntegrationTest // send invitation blogSharingManager0 - .sendInvitation(blog2.getId(), contactId1From0, "Hi!"); + .sendInvitation(blog2.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // invitee has own blog and that of the sharer assertEquals(2, blogManager1.getBlogs().size()); @@ -194,7 +196,8 @@ public class BlogSharingIntegrationTest // send invitation blogSharingManager0 - .sendInvitation(blog2.getId(), contactId1From0, null); + .sendInvitation(blog2.getId(), contactId1From0, null, + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -251,7 +254,8 @@ public class BlogSharingIntegrationTest // send invitation blogSharingManager0 - .sendInvitation(blog2.getId(), contactId1From0, "Hi!"); + .sendInvitation(blog2.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -313,7 +317,8 @@ public class BlogSharingIntegrationTest // sharer sends invitation for 2's blog to 1 blogSharingManager0 - .sendInvitation(blog2.getId(), contactId1From0, "Hi!"); + .sendInvitation(blog2.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -349,7 +354,8 @@ public class BlogSharingIntegrationTest // send invitation blogSharingManager0 - .sendInvitation(blog2.getId(), contactId1From0, "Hi!"); + .sendInvitation(blog2.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -399,7 +405,8 @@ public class BlogSharingIntegrationTest // sharer sends invitation for 2's blog to 1 blogSharingManager0 - .sendInvitation(blog2.getId(), contactId1From0, "Hi!"); + .sendInvitation(blog2.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingIntegrationTest.java index 72c0156b20f614d8ef8960743fe01840436a5641..3452025b9d3acacb709622b61b3971b2f3e82760 100644 --- a/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingIntegrationTest.java @@ -2,19 +2,14 @@ package org.briarproject.briar.sharing; import net.jodah.concurrentunit.Waiter; -import org.briarproject.bramble.api.Bytes; import org.briarproject.bramble.api.contact.Contact; -import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Metadata; import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.test.TestDatabaseModule; -import org.briarproject.briar.api.client.MessageQueueManager; -import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.client.ProtocolStateException; import org.briarproject.briar.api.forum.Forum; import org.briarproject.briar.api.forum.ForumInvitationRequest; import org.briarproject.briar.api.forum.ForumInvitationResponse; @@ -39,11 +34,7 @@ import java.util.Collection; import java.util.List; import static junit.framework.Assert.assertNotNull; -import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getRandomString; -import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT_LENGTH; -import static org.briarproject.briar.api.forum.ForumSharingManager.CLIENT_ID; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -115,7 +106,8 @@ public class ForumSharingIntegrationTest // send invitation forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, "Hi!"); + .sendInvitation(forum0.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -171,7 +163,8 @@ public class ForumSharingIntegrationTest // send invitation forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, null); + .sendInvitation(forum0.getId(), contactId1From0, null, + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -227,7 +220,8 @@ public class ForumSharingIntegrationTest // send invitation forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, "Hi!"); + .sendInvitation(forum0.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -245,9 +239,8 @@ public class ForumSharingIntegrationTest assertTrue(forumManager1.getForums().contains(forum0)); // sharer shares forum with invitee - Contact c1 = contactManager0.getContact(contactId1From0); assertTrue(forumSharingManager0.getSharedWith(forum0.getId()) - .contains(c1)); + .contains(contact1From0)); // invitee gets forum shared by sharer Contact contact0 = contactManager1.getContact(contactId1From0); assertTrue(forumSharingManager1.getSharedWith(forum0.getId()) @@ -265,14 +258,16 @@ public class ForumSharingIntegrationTest // sharer no longer shares forum with invitee assertFalse(forumSharingManager0.getSharedWith(forum0.getId()) - .contains(c1)); + .contains(contact1From0)); // invitee no longer gets forum shared by sharer assertFalse(forumSharingManager1.getSharedWith(forum0.getId()) .contains(contact0)); - // forum can be shared again - assertTrue(forumSharingManager0.canBeShared(forum0.getId(), c1)); - Contact c0 = contactManager1.getContact(contactId0From1); - assertTrue(forumSharingManager1.canBeShared(forum0.getId(), c0)); + // forum can be shared again by sharer + assertTrue(forumSharingManager0 + .canBeShared(forum0.getId(), contact1From0)); + // invitee that left can not share again + assertFalse(forumSharingManager1 + .canBeShared(forum0.getId(), contact0From1)); } @Test @@ -282,7 +277,8 @@ public class ForumSharingIntegrationTest // send invitation forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, null); + .sendInvitation(forum0.getId(), contactId1From0, null, + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -336,7 +332,8 @@ public class ForumSharingIntegrationTest // send invitation forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, null); + .sendInvitation(forum0.getId(), contactId1From0, null, + clock.currentTimeMillis()); // sharer un-subscribes from forum forumManager0.removeForum(forum0); @@ -360,7 +357,8 @@ public class ForumSharingIntegrationTest // send invitation forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, null); + .sendInvitation(forum0.getId(), contactId1From0, null, + clock.currentTimeMillis()); // sharer un-subscribes from forum forumManager0.removeForum(forum0); @@ -375,73 +373,15 @@ public class ForumSharingIntegrationTest assertEquals(1, forumManager1.getForums().size()); } - @Test - public void testSessionIdReuse() throws Exception { - // initialize and let invitee accept all requests - listenToEvents(true); - - // send invitation - forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, "Hi!"); - - // sync first request message - sync0To1(1, true); - eventWaiter.await(TIMEOUT, 1); - assertTrue(listener1.requestReceived); - - // sync response back - sync1To0(1, true); - eventWaiter.await(TIMEOUT, 1); - assertTrue(listener0.responseReceived); - - // forum was added successfully - assertEquals(1, forumManager1.getForums().size()); - - // reset event received state - listener1.requestReceived = false; - - // get SessionId from invitation - List<InvitationMessage> list = new ArrayList<InvitationMessage>( - forumSharingManager1 - .getInvitationMessages(contactId0From1)); - assertEquals(2, list.size()); - InvitationMessage msg = list.get(0); - SessionId sessionId = msg.getSessionId(); - assertEquals(sessionId, list.get(1).getSessionId()); - - // get all sorts of stuff needed to send a message - MessageQueueManager queue = c0.getMessageQueueManager(); - Contact c1 = contactManager0.getContact(contactId1From0); - Group group = contactGroupFactory.createContactGroup(CLIENT_ID, c1); - long time = clock.currentTimeMillis(); - BdfList bodyList = - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId.getBytes(), - getRandomString(42), getRandomBytes(FORUM_SALT_LENGTH)); - byte[] body = clientHelper.toByteArray(bodyList); - - // add the message to the queue - Transaction txn = db0.startTransaction(false); - try { - queue.sendMessage(txn, group, time, body, new Metadata()); - db0.commitTransaction(txn); - } finally { - db0.endTransaction(txn); - } - - // actually send the message - sync0To1(1, false); - // make sure there was no new request received - assertFalse(listener1.requestReceived); - } - - @Test + @Test(expected = ProtocolStateException.class) public void testSharingSameForumWithEachOther() throws Exception { // initialize and let invitee accept all requests listenToEvents(true); // send invitation forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, "Hi!"); + .sendInvitation(forum0.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -455,24 +395,12 @@ public class ForumSharingIntegrationTest // forum was added successfully assertEquals(1, forumManager1.getForums().size()); - assertEquals(2, - forumSharingManager0.getInvitationMessages(contactId1From0) - .size()); // invitee now shares same forum back forumSharingManager1.sendInvitation(forum0.getId(), contactId0From1, - "I am re-sharing this forum with you."); - - // sync re-share invitation - sync1To0(1, false); - - // make sure that no new request was received - assertFalse(listener0.requestReceived); - assertEquals(2, - forumSharingManager0.getInvitationMessages(contactId1From0) - .size()); - assertEquals(0, forumSharingManager0.getInvitations().size()); + "I am re-sharing this forum with you.", + clock.currentTimeMillis()); } @Test @@ -482,62 +410,48 @@ public class ForumSharingIntegrationTest // invitee adds the same forum Transaction txn = db1.startTransaction(false); - db1.addGroup(txn, forum0.getGroup()); + forumManager1.addForum(txn, forum0); db1.commitTransaction(txn); db1.endTransaction(txn); // send invitation forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, "Hi!"); + .sendInvitation(forum0.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // invitee now shares same forum back forumSharingManager1.sendInvitation(forum0.getId(), - contactId0From1, "I am re-sharing this forum with you."); + contactId0From1, "I am re-sharing this forum with you.", + clock.currentTimeMillis()); - // find out who should be Alice, because of random keys - Bytes key0 = new Bytes(author0.getPublicKey()); - Bytes key1 = new Bytes(author1.getPublicKey()); + // prevent automatic responses + respond = false; // only now sync first request message - boolean alice = key1.compareTo(key0) < 0; - if (alice) { - sync0To1(1, false); - assertFalse(listener1.requestReceived); - } else { - sync0To1(1, true); - eventWaiter.await(TIMEOUT, 1); - assertTrue(listener1.requestReceived); - } + sync0To1(1, true); + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener1.requestReceived); - // sync second invitation - alice = key0.compareTo(key1) < 0; - if (alice) { - sync1To0(1, false); - assertFalse(listener0.requestReceived); - - // sharer did not receive request, but response to own request - eventWaiter.await(TIMEOUT, 1); - assertTrue(listener0.responseReceived); - - assertEquals(2, forumSharingManager0 - .getInvitationMessages(contactId1From0).size()); - assertEquals(3, forumSharingManager1 - .getInvitationMessages(contactId0From1).size()); - } else { - sync1To0(1, true); - eventWaiter.await(TIMEOUT, 1); - assertTrue(listener0.requestReceived); - - // send response from sharer to invitee and make sure it arrived - sync0To1(1, true); - eventWaiter.await(TIMEOUT, 1); - assertTrue(listener1.responseReceived); - - assertEquals(3, forumSharingManager0 - .getInvitationMessages(contactId1From0).size()); - assertEquals(2, forumSharingManager1 - .getInvitationMessages(contactId0From1).size()); - } + // sync second invitation which counts as accept + sync1To0(1, true); + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener0.requestReceived); + + // both peers should share the forum with each other now + assertTrue(forumSharingManager0.getSharedWith(forum0.getId()) + .contains(contact1From0)); + assertTrue(forumSharingManager1.getSharedWith(forum0.getId()) + .contains(contact0From1)); + + // and both have each other's invitations (and no response) + assertEquals(2, forumSharingManager0 + .getInvitationMessages(contactId1From0).size()); + assertEquals(2, forumSharingManager1 + .getInvitationMessages(contactId0From1).size()); + + // there are no more open invitations + assertTrue(forumSharingManager0.getInvitations().isEmpty()); + assertTrue(forumSharingManager1.getInvitations().isEmpty()); } @Test @@ -547,7 +461,8 @@ public class ForumSharingIntegrationTest // send invitation forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, "Hi!"); + .sendInvitation(forum0.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -564,18 +479,12 @@ public class ForumSharingIntegrationTest assertEquals(1, forumSharingManager0.getSharedWith(forum0.getId()).size()); - // remember SessionId from invitation - List<InvitationMessage> list = new ArrayList<InvitationMessage>( - forumSharingManager1 - .getInvitationMessages(contactId0From1)); - assertEquals(2, list.size()); - InvitationMessage msg = list.get(0); - SessionId sessionId = msg.getSessionId(); - assertEquals(sessionId, list.get(1).getSessionId()); - // contacts now remove each other removeAllContacts(); + // invitee still has forum + assertEquals(1, forumManager1.getForums().size()); + // make sure sharer does share the forum with nobody now assertEquals(0, forumSharingManager0.getSharedWith(forum0.getId()).size()); @@ -584,35 +493,30 @@ public class ForumSharingIntegrationTest addDefaultContacts(); addContacts1And2(); - // get all sorts of stuff needed to send a message - MessageQueueManager queue = c0.getMessageQueueManager(); - Contact c1 = contactManager0.getContact(contactId1From0); - Group group = contactGroupFactory.createContactGroup(CLIENT_ID, c1); - long time = clock.currentTimeMillis(); - - // construct a new message re-using the old SessionId - BdfList bodyList = BdfList.of(SHARE_MSG_TYPE_INVITATION, - sessionId.getBytes(), - getRandomString(42), - getRandomBytes(FORUM_SALT_LENGTH) - ); - byte[] body = clientHelper.toByteArray(bodyList); - - // add the message to the queue - Transaction txn = db0.startTransaction(false); - try { - queue.sendMessage(txn, group, time, body, new Metadata()); - db0.commitTransaction(txn); - } finally { - db0.endTransaction(txn); - } + // forum can be shared with contacts again + assertTrue(forumSharingManager0 + .canBeShared(forum0.getId(), contact1From0)); + assertTrue(forumSharingManager0 + .canBeShared(forum0.getId(), contact2From0)); + // send invitation + forumSharingManager0 + .sendInvitation(forum0.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); - // actually send the message + // sync first request message sync0To1(1, true); eventWaiter.await(TIMEOUT, 1); - // make sure the new request was received with the same sessionId - // as proof that the state got deleted along with contacts assertTrue(listener1.requestReceived); + + // sync response back + sync1To0(1, true); + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener0.responseReceived); + + // forum is still there + assertEquals(1, forumManager1.getForums().size()); + assertEquals(1, + forumSharingManager0.getSharedWith(forum0.getId()).size()); } @Test @@ -633,14 +537,16 @@ public class ForumSharingIntegrationTest // send invitation forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, "Hi!"); + .sendInvitation(forum0.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); // second sharer sends invitation for same forum assertTrue(contactId1From2 != null); forumSharingManager2 - .sendInvitation(forum0.getId(), contactId1From2, null); + .sendInvitation(forum0.getId(), contactId1From2, null, + clock.currentTimeMillis()); // sync second request message sync2To1(1, true); @@ -650,13 +556,6 @@ public class ForumSharingIntegrationTest assertEquals(1, forums.size()); assertEquals(2, forums.iterator().next().getNewSharers().size()); assertEquals(forum0, forums.iterator().next().getShareable()); - assertEquals(2, - forumSharingManager1.getSharedWith(forum0.getId()).size()); - - // make sure both sharers actually share the forum - Collection<Contact> contacts = - forumSharingManager1.getSharedWith(forum0.getId()); - assertEquals(2, contacts.size()); // answer second request assertNotNull(contactId2From1); @@ -675,6 +574,11 @@ public class ForumSharingIntegrationTest sync1To0(1, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.responseReceived); + + // make sure both sharers actually share the forum + Collection<Contact> contacts = + forumSharingManager1.getSharedWith(forum0.getId()); + assertEquals(2, contacts.size()); } @Test @@ -684,7 +588,8 @@ public class ForumSharingIntegrationTest // send invitation forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, "Hi!"); + .sendInvitation(forum0.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -745,7 +650,8 @@ public class ForumSharingIntegrationTest // send invitation again forumSharingManager0 - .sendInvitation(forum0.getId(), contactId1From0, "Hi!"); + .sendInvitation(forum0.getId(), contactId1From0, "Hi!", + clock.currentTimeMillis()); // sync first request message sync0To1(1, true); @@ -799,8 +705,10 @@ public class ForumSharingIntegrationTest requestReceived = true; Forum f = event.getShareable(); try { - Contact c = contactManager0.getContact(contactId1From0); - forumSharingManager0.respondToInvitation(f, c, true); + if (respond) { + Contact c = contactManager0.getContact(contactId1From0); + forumSharingManager0.respondToInvitation(f, c, true); + } } catch (DbException ex) { eventWaiter.rethrow(ex); } finally { @@ -814,7 +722,6 @@ public class ForumSharingIntegrationTest private class InviteeListener implements EventListener { private volatile boolean requestReceived = false; - private volatile boolean responseReceived = false; private final boolean accept, answer; @@ -836,13 +743,13 @@ public class ForumSharingIntegrationTest if (!answer) return; Forum f = event.getShareable(); try { - eventWaiter.assertEquals(1, - forumSharingManager1.getInvitations().size()); - SharingInvitationItem invitation = - forumSharingManager1.getInvitations().iterator() - .next(); - eventWaiter.assertEquals(f, invitation.getShareable()); if (respond) { + eventWaiter.assertEquals(1, + forumSharingManager1.getInvitations().size()); + SharingInvitationItem invitation = + forumSharingManager1.getInvitations().iterator() + .next(); + eventWaiter.assertEquals(f, invitation.getShareable()); Contact c = contactManager1 .getContact(event.getContactId()); @@ -859,7 +766,6 @@ public class ForumSharingIntegrationTest ForumInvitationResponseReceivedEvent event = (ForumInvitationResponseReceivedEvent) e; eventWaiter.assertEquals(contactId0From1, event.getContactId()); - responseReceived = true; eventWaiter.resume(); } } diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java index 52887134cf3c0945aac3d2ef02a4d5bc1bdfb7b2..c7d2d98f26c03346953564585671a41d0f7f5c4a 100644 --- a/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java @@ -4,340 +4,314 @@ import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.UniqueId; import org.briarproject.bramble.api.client.BdfMessageContext; import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.test.TestUtils; import org.briarproject.bramble.test.ValidatorTestCase; -import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.forum.Forum; +import org.briarproject.briar.api.forum.ForumFactory; +import org.jmock.Expectations; import org.junit.Test; +import java.util.Collection; + import javax.annotation.Nullable; -import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME; -import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT; +import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT_LENGTH; import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH; -import static org.briarproject.briar.api.sharing.SharingConstants.INVITATION_MSG; -import static org.briarproject.briar.api.sharing.SharingConstants.LOCAL; import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH; -import static org.briarproject.briar.api.sharing.SharingConstants.SESSION_ID; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION; -import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE; -import static org.briarproject.briar.api.sharing.SharingConstants.TIME; -import static org.briarproject.briar.api.sharing.SharingConstants.TYPE; +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.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class ForumSharingValidatorTest extends ValidatorTestCase { - private final SessionId sessionId = new SessionId(TestUtils.getRandomId()); + private final MessageEncoder messageEncoder = + context.mock(MessageEncoder.class); + private final ForumFactory forumFactory = context.mock(ForumFactory.class); + private final ForumSharingValidator v = + new ForumSharingValidator(messageEncoder, clientHelper, + metadataEncoder, clock, forumFactory); + + private final MessageId previousMsgId = new MessageId(getRandomId()); private final String forumName = TestUtils.getRandomString(MAX_FORUM_NAME_LENGTH); private final byte[] salt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH); + private final Forum forum = new Forum(group, forumName, salt); + private final BdfList descriptor = BdfList.of(forumName, salt); private final String content = TestUtils.getRandomString(MAX_INVITATION_MESSAGE_LENGTH); + private final BdfDictionary meta = + BdfDictionary.of(new BdfEntry("meta", "data")); @Test public void testAcceptsInvitationWithContent() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + expectCreateForum(forumName); + expectEncodeMetadata(INVITE); + BdfMessageContext messageContext = v.validateMessage(message, group, + BdfList.of(INVITE.getValue(), previousMsgId, descriptor, + content)); + assertExpectedContext(messageContext, previousMsgId); + } + + @Test + public void testAcceptsInvitationWithNullContent() throws Exception { + expectCreateForum(forumName); + expectEncodeMetadata(INVITE); BdfMessageContext messageContext = v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName, - salt, content)); - assertExpectedContextForInvitation(messageContext, forumName, content); + BdfList.of(INVITE.getValue(), previousMsgId, descriptor, null)); + assertExpectedContext(messageContext, previousMsgId); } @Test - public void testAcceptsInvitationWithoutContent() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + public void testAcceptsInvitationWithNullPreviousMsgId() throws Exception { + expectCreateForum(forumName); + expectEncodeMetadata(INVITE); BdfMessageContext messageContext = v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName, - salt)); - assertExpectedContextForInvitation(messageContext, forumName, null); + BdfList.of(INVITE.getValue(), null, descriptor, null)); + assertExpectedContext(messageContext, null); } @Test public void testAcceptsAccept() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + expectEncodeMetadata(ACCEPT); BdfMessageContext messageContext = v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_ACCEPT, sessionId)); - assertExpectedContext(messageContext, SHARE_MSG_TYPE_ACCEPT); + BdfList.of(ACCEPT.getValue(), groupId, previousMsgId)); + assertExpectedContext(messageContext, previousMsgId); } @Test public void testAcceptsDecline() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + expectEncodeMetadata(DECLINE); BdfMessageContext messageContext = v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_DECLINE, sessionId)); - assertExpectedContext(messageContext, SHARE_MSG_TYPE_DECLINE); + BdfList.of(DECLINE.getValue(), groupId, previousMsgId)); + assertExpectedContext(messageContext, previousMsgId); } @Test public void testAcceptsLeave() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + expectEncodeMetadata(LEAVE); BdfMessageContext messageContext = v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_LEAVE, sessionId)); - assertExpectedContext(messageContext, SHARE_MSG_TYPE_LEAVE); + BdfList.of(LEAVE.getValue(), groupId, previousMsgId)); + assertExpectedContext(messageContext, previousMsgId); } @Test public void testAcceptsAbort() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + expectEncodeMetadata(ABORT); BdfMessageContext messageContext = v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_ABORT, sessionId)); - assertExpectedContext(messageContext, SHARE_MSG_TYPE_ABORT); + BdfList.of(ABORT.getValue(), groupId, previousMsgId)); + assertExpectedContext(messageContext, previousMsgId); } @Test(expected = FormatException.class) public void testRejectsNullMessageType() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); - v.validateMessage(message, group, BdfList.of(null, sessionId)); + v.validateMessage(message, group, + BdfList.of(null, groupId, previousMsgId)); } @Test(expected = FormatException.class) public void testRejectsNonLongMessageType() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); - v.validateMessage(message, group, BdfList.of("", sessionId)); + v.validateMessage(message, group, + BdfList.of("", groupId, previousMsgId)); } @Test(expected = FormatException.class) public void testRejectsInvalidMessageType() throws Exception { - int invalidMessageType = SHARE_MSG_TYPE_ABORT + 1; - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + int invalidMessageType = ABORT.getValue() + 1; v.validateMessage(message, group, - BdfList.of(invalidMessageType, sessionId)); + BdfList.of(invalidMessageType, groupId, previousMsgId)); } @Test(expected = FormatException.class) public void testRejectsNullSessionId() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_ABORT, null)); + BdfList.of(ABORT.getValue(), null, previousMsgId)); } @Test(expected = FormatException.class) public void testRejectsNonRawSessionId() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); - v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_ABORT, 123)); + v.validateMessage(message, group, BdfList.of(ABORT.getValue(), 123)); } @Test(expected = FormatException.class) public void testRejectsTooShortSessionId() throws Exception { - byte[] invalidSessionId = TestUtils.getRandomBytes(UniqueId.LENGTH - 1); - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + byte[] invalidGroupId = TestUtils.getRandomBytes(UniqueId.LENGTH - 1); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_ABORT, invalidSessionId)); + BdfList.of(ABORT.getValue(), invalidGroupId, previousMsgId)); } @Test(expected = FormatException.class) public void testRejectsTooLongSessionId() throws Exception { - byte[] invalidSessionId = TestUtils.getRandomBytes(UniqueId.LENGTH + 1); - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + byte[] invalidGroupId = TestUtils.getRandomBytes(UniqueId.LENGTH + 1); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_ABORT, invalidSessionId)); + BdfList.of(ABORT.getValue(), invalidGroupId, previousMsgId)); } @Test(expected = FormatException.class) public void testRejectsTooShortBodyForAbort() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); - v.validateMessage(message, group, BdfList.of(SHARE_MSG_TYPE_ABORT)); + v.validateMessage(message, group, + BdfList.of(ABORT.getValue(), groupId)); } @Test(expected = FormatException.class) public void testRejectsTooLongBodyForAbort() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_ABORT, sessionId, 123)); + BdfList.of(ABORT.getValue(), groupId, previousMsgId, 123)); } @Test(expected = FormatException.class) public void testRejectsTooShortBodyForInvitation() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName)); + BdfList.of(INVITE.getValue(), previousMsgId, descriptor)); } @Test(expected = FormatException.class) public void testRejectsTooLongBodyForInvitation() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName, - salt, content, 123)); + BdfList.of(INVITE.getValue(), previousMsgId, descriptor, null, + 123)); } @Test(expected = FormatException.class) public void testRejectsNullForumName() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + BdfList invalidDescriptor = BdfList.of(null, salt); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, null, - salt, content)); + BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, + null)); } @Test(expected = FormatException.class) public void testRejectsNonStringForumName() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + BdfList invalidDescriptor = BdfList.of(123, salt); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, 123, - salt, content)); + BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, + null)); } @Test(expected = FormatException.class) public void testRejectsTooShortForumName() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + BdfList invalidDescriptor = BdfList.of("", salt); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, "", - salt, content)); + BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, + null)); } @Test public void testAcceptsMinLengthForumName() throws Exception { String shortForumName = TestUtils.getRandomString(1); - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + BdfList validDescriptor = BdfList.of(shortForumName, salt); + expectCreateForum(shortForumName); + expectEncodeMetadata(INVITE); BdfMessageContext messageContext = v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, shortForumName, - salt, content)); - assertExpectedContextForInvitation(messageContext, shortForumName, - content); + BdfList.of(INVITE.getValue(), previousMsgId, validDescriptor, + null)); + assertExpectedContext(messageContext, previousMsgId); } @Test(expected = FormatException.class) public void testRejectsTooLongForumName() throws Exception { String invalidForumName = TestUtils.getRandomString(MAX_FORUM_NAME_LENGTH + 1); - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + BdfList invalidDescriptor = BdfList.of(invalidForumName, salt); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, - invalidForumName, salt, content)); + BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, + null)); } @Test(expected = FormatException.class) public void testRejectsNullSalt() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + BdfList invalidDescriptor = BdfList.of(forumName, null); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName, - null, content)); + BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, + null)); } @Test(expected = FormatException.class) public void testRejectsNonRawSalt() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + BdfList invalidDescriptor = BdfList.of(forumName, 123); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName, - 123, content)); + BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, + null)); } @Test(expected = FormatException.class) public void testRejectsTooShortSalt() throws Exception { byte[] invalidSalt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH - 1); - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + BdfList invalidDescriptor = BdfList.of(forumName, invalidSalt); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName, - invalidSalt, content)); + BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, + null)); } @Test(expected = FormatException.class) public void testRejectsTooLongSalt() throws Exception { byte[] invalidSalt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH + 1); - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + BdfList invalidDescriptor = BdfList.of(forumName, invalidSalt); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName, - invalidSalt, content)); - } - - @Test(expected = FormatException.class) - public void testRejectsNullContent() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); - v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName, - salt, null)); + BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, + null)); } @Test(expected = FormatException.class) public void testRejectsNonStringContent() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + expectCreateForum(forumName); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName, - salt, 123)); + BdfList.of(INVITE.getValue(), previousMsgId, descriptor, + 123)); } @Test public void testAcceptsMinLengthContent() throws Exception { - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + expectCreateForum(forumName); + expectEncodeMetadata(INVITE); BdfMessageContext messageContext = v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName, - salt, "")); - assertExpectedContextForInvitation(messageContext, forumName, ""); + BdfList.of(INVITE.getValue(), previousMsgId, descriptor, "1")); + assertExpectedContext(messageContext, previousMsgId); } @Test(expected = FormatException.class) public void testRejectsTooLongContent() throws Exception { String invalidContent = TestUtils.getRandomString(MAX_INVITATION_MESSAGE_LENGTH + 1); - ForumSharingValidator v = new ForumSharingValidator(clientHelper, - metadataEncoder, clock); + expectCreateForum(forumName); v.validateMessage(message, group, - BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName, - salt, invalidContent)); + BdfList.of(INVITE.getValue(), previousMsgId, descriptor, + invalidContent)); } - private void assertExpectedContextForInvitation( - BdfMessageContext messageContext, String forumName, - @Nullable String content) throws FormatException { - BdfDictionary meta = messageContext.getDictionary(); - if (content == null) { - assertEquals(6, meta.size()); - } else { - assertEquals(7, meta.size()); - assertEquals(content, meta.getString(INVITATION_MSG)); - } - assertEquals(forumName, meta.getString(FORUM_NAME)); - assertEquals(salt, meta.getRaw(FORUM_SALT)); - assertEquals(SHARE_MSG_TYPE_INVITATION, meta.getLong(TYPE).intValue()); - assertEquals(sessionId.getBytes(), meta.getRaw(SESSION_ID)); - assertFalse(meta.getBoolean(LOCAL)); - assertEquals(timestamp, meta.getLong(TIME).longValue()); - assertEquals(0, messageContext.getDependencies().size()); + private void expectCreateForum(final String name) { + context.checking(new Expectations() {{ + oneOf(forumFactory).createForum(name, salt); + will(returnValue(forum)); + }}); + } + + private void expectEncodeMetadata(final MessageType type) { + context.checking(new Expectations() {{ + oneOf(messageEncoder) + .encodeMetadata(type, groupId, timestamp, false, false, + false, false); + will(returnValue(meta)); + }}); } private void assertExpectedContext(BdfMessageContext messageContext, - int type) throws FormatException { - BdfDictionary meta = messageContext.getDictionary(); - assertEquals(4, meta.size()); - assertEquals(type, meta.getLong(TYPE).intValue()); - assertEquals(sessionId.getBytes(), meta.getRaw(SESSION_ID)); - assertFalse(meta.getBoolean(LOCAL)); - assertEquals(timestamp, meta.getLong(TIME).longValue()); - assertEquals(0, messageContext.getDependencies().size()); + @Nullable MessageId previousMsgId) throws FormatException { + Collection<MessageId> dependencies = messageContext.getDependencies(); + if (previousMsgId == null) { + assertTrue(dependencies.isEmpty()); + } else { + assertEquals(1, dependencies.size()); + assertTrue(dependencies.contains(previousMsgId)); + } + assertEquals(meta, messageContext.getDictionary()); } + }