From 9bef114c350cfb2256ca42b372b57c0296d86b25 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 26 Apr 2016 20:26:46 -0300 Subject: [PATCH] Forum Sharing Client backend This commit replaces the old ForumSharingManagerImpl with a new one which is based on state machines and the ProtocolEngine. There is a SharerEngine and a InviteeEngine that take care of state transitions, messages, events and trigger actions to be carried out by the ForumSharingManagerImpl. This is all very similar to the Introduction Client. The general sharing paradigm has been changed from sharing as a state to sharing as an action. Now the UI can allow users to invite contacts to forums. The contacts can accept or decline the invitiation. Also, the Forum Sharing Manger is notified when users leave a forum. Closes #322 --- .../contact/ConversationIntroductionItem.java | 2 +- .../android/contact/ConversationItem.java | 10 +- .../forum/AvailableForumsActivity.java | 1 - .../android/forum/ShareForumActivity.java | 21 - .../event/ForumInvitationReceivedEvent.java | 24 + .../ForumInvitationResponseReceivedEvent.java | 24 + .../api/forum/ForumConstants.java | 34 + .../api/forum/ForumInvitationMessage.java | 48 + .../api/forum/ForumSharingManager.java | 30 +- .../briarproject/api/forum/InviteeAction.java | 34 + .../api/forum/InviteeProtocolState.java | 62 ++ .../briarproject/api/forum/SharerAction.java | 34 + .../api/forum/SharerProtocolState.java | 62 ++ .../api/introduction/IntroductionMessage.java | 31 +- .../api/messaging/BaseMessage.java | 45 + .../api/messaging/PrivateMessageHeader.java | 37 +- .../forum/ForumListValidator.java | 48 - .../org/briarproject/forum/ForumModule.java | 21 +- .../forum/ForumSharingManagerImpl.java | 871 ++++++++++++++---- .../forum/ForumSharingValidator.java | 81 ++ .../org/briarproject/forum/InviteeEngine.java | 265 ++++++ .../org/briarproject/forum/SharerEngine.java | 264 ++++++ ...st.java => ForumSharingValidatorTest.java} | 2 +- 23 files changed, 1713 insertions(+), 338 deletions(-) create mode 100644 briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java create mode 100644 briar-api/src/org/briarproject/api/event/ForumInvitationResponseReceivedEvent.java create mode 100644 briar-api/src/org/briarproject/api/forum/ForumInvitationMessage.java create mode 100644 briar-api/src/org/briarproject/api/forum/InviteeAction.java create mode 100644 briar-api/src/org/briarproject/api/forum/InviteeProtocolState.java create mode 100644 briar-api/src/org/briarproject/api/forum/SharerAction.java create mode 100644 briar-api/src/org/briarproject/api/forum/SharerProtocolState.java create mode 100644 briar-api/src/org/briarproject/api/messaging/BaseMessage.java delete mode 100644 briar-core/src/org/briarproject/forum/ForumListValidator.java create mode 100644 briar-core/src/org/briarproject/forum/ForumSharingValidator.java create mode 100644 briar-core/src/org/briarproject/forum/InviteeEngine.java create mode 100644 briar-core/src/org/briarproject/forum/SharerEngine.java rename briar-tests/src/org/briarproject/forum/{ForumListValidatorTest.java => ForumSharingValidatorTest.java} (77%) diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java index e955ea3a4..565f9f1f8 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java @@ -8,7 +8,7 @@ abstract class ConversationIntroductionItem extends ConversationItem { private boolean answered; public ConversationIntroductionItem(IntroductionRequest ir) { - super(ir.getMessageId(), ir.getTime()); + super(ir.getMessageId(), ir.getTimestamp()); this.ir = ir; this.answered = ir.wasAnswered(); diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationItem.java index 76eb803a3..2fc96b6ab 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java @@ -69,7 +69,7 @@ public abstract class ConversationItem { ir.getName()); } return new ConversationNoticeOutItem(ir.getMessageId(), text, - ir.getTime(), ir.isSent(), ir.isSeen()); + ir.getTimestamp(), ir.isSent(), ir.isSeen()); } else { String text; if (ir.wasAccepted()) { @@ -88,7 +88,7 @@ public abstract class ConversationItem { } } return new ConversationNoticeInItem(ir.getMessageId(), text, - ir.getTime(), ir.isRead()); + ir.getTimestamp(), ir.isRead()); } } @@ -98,9 +98,9 @@ public abstract class ConversationItem { public static ConversationItem from(IntroductionMessage im) { if (im.isLocal()) return new ConversationNoticeOutItem(im.getMessageId(), "", - im.getTime(), false, false); - return new ConversationNoticeInItem(im.getMessageId(), "", im.getTime(), - im.isRead()); + im.getTimestamp(), false, false); + return new ConversationNoticeInItem(im.getMessageId(), "", + im.getTimestamp(), im.isRead()); } protected interface OutgoingItem { diff --git a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java index eb4ade9ab..f685e0d06 100644 --- a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java +++ b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java @@ -171,7 +171,6 @@ implements EventListener, OnItemClickListener { public void run() { try { forumManager.addForum(f); - forumSharingManager.setSharedWith(f.getId(), shared); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java index cccecbc7a..ccb0b365d 100644 --- a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java @@ -91,7 +91,6 @@ public class ShareForumActivity extends BriarActivity implements onBackPressed(); return true; case R.id.action_share_forum: - storeVisibility(); return true; default: return super.onOptionsItemSelected(item); @@ -140,26 +139,6 @@ public class ShareForumActivity extends BriarActivity implements }); } - private void storeVisibility() { - runOnDbThread(new Runnable() { - public void run() { - try { - long now = System.currentTimeMillis(); - Collection selected = - adapter.getSelectedContactIds(); - forumSharingManager.setSharedWith(groupId, selected); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Update took " + duration + " ms"); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - } - finishOnUiThread(); - } - }); - } - @Override public void onItemClick(View view, ContactListItem item) { ((SelectableContactListItem) item).toggleSelected(); diff --git a/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java b/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java new file mode 100644 index 000000000..1d8e7b5f7 --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java @@ -0,0 +1,24 @@ +package org.briarproject.api.event; + +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.forum.Forum; +import org.briarproject.api.introduction.IntroductionRequest; + +public class ForumInvitationReceivedEvent extends Event { + + private final Forum forum; + private final ContactId contactId; + + public ForumInvitationReceivedEvent(Forum forum, ContactId contactId) { + this.forum = forum; + this.contactId = contactId; + } + + public Forum getForum() { + return forum; + } + + public ContactId getContactId() { + return contactId; + } +} diff --git a/briar-api/src/org/briarproject/api/event/ForumInvitationResponseReceivedEvent.java b/briar-api/src/org/briarproject/api/event/ForumInvitationResponseReceivedEvent.java new file mode 100644 index 000000000..1e7992403 --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/ForumInvitationResponseReceivedEvent.java @@ -0,0 +1,24 @@ +package org.briarproject.api.event; + +import org.briarproject.api.contact.ContactId; + +public class ForumInvitationResponseReceivedEvent extends Event { + + private final String forumName; + private final ContactId contactId; + + public ForumInvitationResponseReceivedEvent(String forumName, + ContactId contactId) { + + this.forumName = forumName; + this.contactId = contactId; + } + + public String getForumName() { + return forumName; + } + + public ContactId getContactId() { + return contactId; + } +} diff --git a/briar-api/src/org/briarproject/api/forum/ForumConstants.java b/briar-api/src/org/briarproject/api/forum/ForumConstants.java index 035a06cd1..dcdee0132 100644 --- a/briar-api/src/org/briarproject/api/forum/ForumConstants.java +++ b/briar-api/src/org/briarproject/api/forum/ForumConstants.java @@ -15,4 +15,38 @@ public interface ForumConstants { /** The maximum length of a forum post's body in bytes. */ int MAX_FORUM_POST_BODY_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024; + + /* Forum Sharing Constants */ + String CONTACT_ID = "contactId"; + String GROUP_ID = "groupId"; + String TO_BE_SHARED_BY_US = "toBeSharedByUs"; + String SHARED_BY_US = "sharedByUs"; + String SHARED_WITH_US = "sharedWithUs"; + String TYPE = "type"; + String SESSION_ID = "sessionId"; + String STORAGE_ID = "storageId"; + String STATE = "state"; + String LOCAL = "local"; + String TIME = "time"; + String READ = "read"; + String IS_SHARER = "isSharer"; + String FORUM_ID = "forumId"; + String FORUM_NAME = "forumName"; + String FORUM_SALT = "forumSalt"; + String INVITATION_MSG = "invitationMsg"; + int SHARE_MSG_TYPE_INVITATION = 1; + int SHARE_MSG_TYPE_ACCEPT = 2; + int SHARE_MSG_TYPE_DECLINE = 3; + int SHARE_MSG_TYPE_LEAVE = 4; + int SHARE_MSG_TYPE_ABORT = 5; + String TASK = "task"; + int TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US = 0; + int TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US = 1; + int TASK_ADD_SHARED_FORUM = 2; + int TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US = 3; + int TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US = 4; + int TASK_SHARE_FORUM = 5; + int TASK_UNSHARE_FORUM_SHARED_BY_US = 6; + int TASK_UNSHARE_FORUM_SHARED_WITH_US = 7; + } diff --git a/briar-api/src/org/briarproject/api/forum/ForumInvitationMessage.java b/briar-api/src/org/briarproject/api/forum/ForumInvitationMessage.java new file mode 100644 index 000000000..b153c27f1 --- /dev/null +++ b/briar-api/src/org/briarproject/api/forum/ForumInvitationMessage.java @@ -0,0 +1,48 @@ +package org.briarproject.api.forum; + +import org.briarproject.api.clients.SessionId; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.messaging.BaseMessage; +import org.briarproject.api.sync.MessageId; + +public class ForumInvitationMessage extends BaseMessage { + + private final SessionId sessionId; + private final ContactId contactId; + private final String forumName, message; + private final boolean available; + + public ForumInvitationMessage(MessageId id, SessionId sessionId, + ContactId contactId, String forumName, String message, + boolean available, long time, boolean local, boolean sent, + boolean seen, boolean read) { + + super(id, time, local, read, sent, seen); + this.sessionId = sessionId; + this.contactId = contactId; + this.forumName = forumName; + this.message = message; + this.available = available; + } + + public SessionId getSessionId() { + return sessionId; + } + + public ContactId getContactId() { + return contactId; + } + + public String getForumName() { + return forumName; + } + + public String getMessage() { + return message; + } + + public boolean isAvailable() { + return available; + } + +} diff --git a/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java index e3e3b1191..9f4cc87b6 100644 --- a/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java +++ b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java @@ -1,5 +1,6 @@ package org.briarproject.api.forum; +import org.briarproject.api.clients.SessionId; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; import org.briarproject.api.db.DbException; @@ -13,20 +14,35 @@ public interface ForumSharingManager { /** Returns the unique ID of the forum sharing client. */ ClientId getClientId(); + /** + * Sends an invitation to share the given forum with the given contact + * and sends an optional message along with it. + */ + void sendForumInvitation(GroupId groupId, ContactId contactId, + String message) throws DbException; + + /** + * Responds to a pending forum invitation + */ + void respondToInvitation(Forum f, boolean accept) throws DbException; + + /** + * Returns all forum sharing messages sent by the Contact + * identified by contactId. + */ + Collection getForumInvitationMessages( + ContactId contactId) throws DbException; + /** Returns all forums to which the user could subscribe. */ Collection getAvailableForums() throws DbException; - /** Returns all contacts who are sharing the given forum with the user. */ + /** Returns all contacts who are sharing the given forum with us. */ Collection getSharedBy(GroupId g) throws DbException; /** Returns the IDs of all contacts with whom the given forum is shared. */ Collection getSharedWith(GroupId g) throws DbException; - /** - * Shares a forum with the given contacts and unshares it with any other - * contacts. - */ - void setSharedWith(GroupId g, Collection shared) - throws DbException; + /** Returns true if the forum not already shared and no invitation is open */ + boolean canBeShared(GroupId g, Contact c) throws DbException; } diff --git a/briar-api/src/org/briarproject/api/forum/InviteeAction.java b/briar-api/src/org/briarproject/api/forum/InviteeAction.java new file mode 100644 index 000000000..212f0861c --- /dev/null +++ b/briar-api/src/org/briarproject/api/forum/InviteeAction.java @@ -0,0 +1,34 @@ +package org.briarproject.api.forum; + +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE; + +public enum InviteeAction { + + LOCAL_ACCEPT, + LOCAL_DECLINE, + LOCAL_LEAVE, + LOCAL_ABORT, + REMOTE_INVITATION, + REMOTE_LEAVE, + REMOTE_ABORT; + + public static InviteeAction getLocal(long type) { + if (type == SHARE_MSG_TYPE_ACCEPT) return LOCAL_ACCEPT; + if (type == SHARE_MSG_TYPE_DECLINE) return LOCAL_DECLINE; + if (type == SHARE_MSG_TYPE_LEAVE) return LOCAL_LEAVE; + if (type == SHARE_MSG_TYPE_ABORT) return LOCAL_ABORT; + return null; + } + + public static InviteeAction getRemote(long type) { + if (type == SHARE_MSG_TYPE_INVITATION) return REMOTE_INVITATION; + if (type == SHARE_MSG_TYPE_LEAVE) return REMOTE_LEAVE; + if (type == SHARE_MSG_TYPE_ABORT) return REMOTE_ABORT; + return null; + } + +} diff --git a/briar-api/src/org/briarproject/api/forum/InviteeProtocolState.java b/briar-api/src/org/briarproject/api/forum/InviteeProtocolState.java new file mode 100644 index 000000000..35d6a880a --- /dev/null +++ b/briar-api/src/org/briarproject/api/forum/InviteeProtocolState.java @@ -0,0 +1,62 @@ +package org.briarproject.api.forum; + +import static org.briarproject.api.forum.InviteeAction.LOCAL_ACCEPT; +import static org.briarproject.api.forum.InviteeAction.LOCAL_DECLINE; +import static org.briarproject.api.forum.InviteeAction.LOCAL_LEAVE; +import static org.briarproject.api.forum.InviteeAction.REMOTE_INVITATION; +import static org.briarproject.api.forum.InviteeAction.REMOTE_LEAVE; + +public enum InviteeProtocolState { + + ERROR(0), + AWAIT_INVITATION(1) { + @Override + public InviteeProtocolState next(InviteeAction a) { + if (a == REMOTE_INVITATION) return AWAIT_LOCAL_RESPONSE; + return ERROR; + } + }, + AWAIT_LOCAL_RESPONSE(2) { + @Override + public InviteeProtocolState next(InviteeAction a) { + if (a == LOCAL_ACCEPT || a == LOCAL_DECLINE) return FINISHED; + if (a == REMOTE_LEAVE) return LEFT; + return ERROR; + } + }, + FINISHED(3) { + @Override + public InviteeProtocolState next(InviteeAction a) { + if (a == LOCAL_LEAVE || a == REMOTE_LEAVE) return LEFT; + return FINISHED; + } + }, + LEFT(4) { + @Override + public InviteeProtocolState next(InviteeAction a) { + if (a == LOCAL_LEAVE) return ERROR; + return LEFT; + } + }; + + private final int value; + + InviteeProtocolState(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static InviteeProtocolState fromValue(int value) { + for (InviteeProtocolState s : values()) { + if (s.value == value) return s; + } + throw new IllegalArgumentException(); + } + + public InviteeProtocolState next(InviteeAction a) { + return this; + } +} diff --git a/briar-api/src/org/briarproject/api/forum/SharerAction.java b/briar-api/src/org/briarproject/api/forum/SharerAction.java new file mode 100644 index 000000000..39796f2c8 --- /dev/null +++ b/briar-api/src/org/briarproject/api/forum/SharerAction.java @@ -0,0 +1,34 @@ +package org.briarproject.api.forum; + +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE; + +public enum SharerAction { + + LOCAL_INVITATION, + LOCAL_LEAVE, + LOCAL_ABORT, + REMOTE_ACCEPT, + REMOTE_DECLINE, + REMOTE_LEAVE, + REMOTE_ABORT; + + public static SharerAction getLocal(long type) { + if (type == SHARE_MSG_TYPE_INVITATION) return LOCAL_INVITATION; + if (type == SHARE_MSG_TYPE_LEAVE) return LOCAL_LEAVE; + if (type == SHARE_MSG_TYPE_ABORT) return LOCAL_ABORT; + return null; + } + + public static SharerAction getRemote(long type) { + if (type == SHARE_MSG_TYPE_ACCEPT) return REMOTE_ACCEPT; + if (type == SHARE_MSG_TYPE_DECLINE) return REMOTE_DECLINE; + if (type == SHARE_MSG_TYPE_LEAVE) return REMOTE_LEAVE; + if (type == SHARE_MSG_TYPE_ABORT) return REMOTE_ABORT; + return null; + } + +} diff --git a/briar-api/src/org/briarproject/api/forum/SharerProtocolState.java b/briar-api/src/org/briarproject/api/forum/SharerProtocolState.java new file mode 100644 index 000000000..b948a9483 --- /dev/null +++ b/briar-api/src/org/briarproject/api/forum/SharerProtocolState.java @@ -0,0 +1,62 @@ +package org.briarproject.api.forum; + +import static org.briarproject.api.forum.SharerAction.LOCAL_INVITATION; +import static org.briarproject.api.forum.SharerAction.LOCAL_LEAVE; +import static org.briarproject.api.forum.SharerAction.REMOTE_ACCEPT; +import static org.briarproject.api.forum.SharerAction.REMOTE_DECLINE; +import static org.briarproject.api.forum.SharerAction.REMOTE_LEAVE; + +public enum SharerProtocolState { + + ERROR(0), + PREPARE_INVITATION(1) { + @Override + public SharerProtocolState next(SharerAction a) { + if (a == LOCAL_INVITATION) return AWAIT_RESPONSE; + return ERROR; + } + }, + AWAIT_RESPONSE(2) { + @Override + public SharerProtocolState next(SharerAction a) { + if (a == REMOTE_ACCEPT || a == REMOTE_DECLINE) return FINISHED; + if (a == LOCAL_LEAVE) return LEFT; + return ERROR; + } + }, + FINISHED(3) { + @Override + public SharerProtocolState next(SharerAction a) { + if (a == LOCAL_LEAVE || a == REMOTE_LEAVE) return LEFT; + return FINISHED; + } + }, + LEFT(4) { + @Override + public SharerProtocolState next(SharerAction a) { + if (a == LOCAL_LEAVE) return ERROR; + return LEFT; + } + }; + + private final int value; + + SharerProtocolState(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static SharerProtocolState fromValue(int value) { + for (SharerProtocolState s : values()) { + if (s.value == value) return s; + } + throw new IllegalArgumentException(); + } + + public SharerProtocolState next(SharerAction a) { + return this; + } +} diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java index 8c9de76df..d860726a7 100644 --- a/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java +++ b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java @@ -1,61 +1,36 @@ package org.briarproject.api.introduction; import org.briarproject.api.clients.SessionId; +import org.briarproject.api.messaging.BaseMessage; import org.briarproject.api.sync.MessageId; import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE; import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER; -abstract public class IntroductionMessage { + public class IntroductionMessage extends BaseMessage { private final SessionId sessionId; private final MessageId messageId; private final int role; - private final long time; - private final boolean local, sent, seen, read; public IntroductionMessage(SessionId sessionId, MessageId messageId, int role, long time, boolean local, boolean sent, boolean seen, boolean read) { + super(messageId, time, local, read, sent, seen); this.sessionId = sessionId; this.messageId = messageId; this.role = role; - this.time = time; - this.local = local; - this.sent = sent; - this.seen = seen; - this.read = read; } public SessionId getSessionId() { return sessionId; } - public long getTime() { - return time; - } - public MessageId getMessageId() { return messageId; } - public boolean isLocal() { - return local; - } - - public boolean isSent() { - return sent; - } - - public boolean isSeen() { - return seen; - } - - public boolean isRead() { - return read; - } - public boolean isIntroducer() { return role == ROLE_INTRODUCER; } diff --git a/briar-api/src/org/briarproject/api/messaging/BaseMessage.java b/briar-api/src/org/briarproject/api/messaging/BaseMessage.java new file mode 100644 index 000000000..c83350a09 --- /dev/null +++ b/briar-api/src/org/briarproject/api/messaging/BaseMessage.java @@ -0,0 +1,45 @@ +package org.briarproject.api.messaging; + +import org.briarproject.api.sync.MessageId; + +public abstract class BaseMessage { + + private final MessageId id; + private final long timestamp; + private final boolean local, read, sent, seen; + + public BaseMessage(MessageId id, long timestamp, boolean local, + boolean read, boolean sent, boolean seen) { + + this.id = id; + this.timestamp = timestamp; + this.local = local; + this.read = read; + this.sent = sent; + this.seen = seen; + } + + public MessageId getId() { + return id; + } + + public long getTimestamp() { + return timestamp; + } + + public boolean isLocal() { + return local; + } + + public boolean isRead() { + return read; + } + + public boolean isSent() { + return sent; + } + + public boolean isSeen() { + return seen; + } +} diff --git a/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java b/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java index 9db8854a1..f1c8eb51f 100644 --- a/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java +++ b/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java @@ -2,50 +2,19 @@ package org.briarproject.api.messaging; import org.briarproject.api.sync.MessageId; -public class PrivateMessageHeader { +public class PrivateMessageHeader extends BaseMessage { - private final MessageId id; - private final long timestamp; private final String contentType; - private final boolean local, read, sent, seen; public PrivateMessageHeader(MessageId id, long timestamp, String contentType, boolean local, boolean read, boolean sent, boolean seen) { - this.id = id; - this.timestamp = timestamp; - this.contentType = contentType; - this.local = local; - this.read = read; - this.sent = sent; - this.seen = seen; - } - public MessageId getId() { - return id; + super(id, timestamp, local, read, sent, seen); + this.contentType = contentType; } public String getContentType() { return contentType; } - - public long getTimestamp() { - return timestamp; - } - - public boolean isLocal() { - return local; - } - - public boolean isRead() { - return read; - } - - public boolean isSent() { - return sent; - } - - public boolean isSeen() { - return seen; - } } diff --git a/briar-core/src/org/briarproject/forum/ForumListValidator.java b/briar-core/src/org/briarproject/forum/ForumListValidator.java deleted file mode 100644 index 9e6947a37..000000000 --- a/briar-core/src/org/briarproject/forum/ForumListValidator.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.briarproject.forum; - -import org.briarproject.api.FormatException; -import org.briarproject.api.clients.ClientHelper; -import org.briarproject.api.data.BdfDictionary; -import org.briarproject.api.data.BdfList; -import org.briarproject.api.data.MetadataEncoder; -import org.briarproject.api.sync.Group; -import org.briarproject.api.sync.Message; -import org.briarproject.api.system.Clock; -import org.briarproject.clients.BdfMessageValidator; - -import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH; -import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH; - -class ForumListValidator extends BdfMessageValidator { - - ForumListValidator(ClientHelper clientHelper, - MetadataEncoder metadataEncoder, Clock clock) { - super(clientHelper, metadataEncoder, clock); - } - - @Override - protected BdfDictionary validateMessage(Message m, Group g, - BdfList body) throws FormatException { - // Version, forum list - checkSize(body, 2); - // Version - long version = body.getLong(0); - if (version < 0) throw new FormatException(); - // Forum list - BdfList forumList = body.getList(1); - for (int i = 0; i < forumList.size(); i++) { - BdfList forum = forumList.getList(i); - // Name, salt - checkSize(forum, 2); - String name = forum.getString(0); - checkLength(name, 1, MAX_FORUM_NAME_LENGTH); - byte[] salt = forum.getRaw(1); - checkLength(salt, FORUM_SALT_LENGTH); - } - // Return the metadata - BdfDictionary meta = new BdfDictionary(); - meta.put("version", version); - meta.put("local", false); - return meta; - } -} diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java index ac0ff2061..e6d7b55ac 100644 --- a/briar-core/src/org/briarproject/forum/ForumModule.java +++ b/briar-core/src/org/briarproject/forum/ForumModule.java @@ -1,6 +1,7 @@ package org.briarproject.forum; import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.clients.MessageQueueManager; import org.briarproject.api.contact.ContactManager; import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.data.MetadataEncoder; @@ -26,11 +27,11 @@ import dagger.Provides; public class ForumModule { public static class EagerSingletons { - @Inject - ForumListValidator forumListValidator; @Inject ForumPostValidator forumPostValidator; @Inject + ForumSharingValidator forumSharingValidator; + @Inject ForumSharingManager forumSharingManager; } @@ -63,13 +64,15 @@ public class ForumModule { @Provides @Singleton - ForumListValidator provideForumListValidator( - ValidationManager validationManager, ClientHelper clientHelper, + ForumSharingValidator provideSharingValidator( + MessageQueueManager messageQueueManager, ClientHelper clientHelper, MetadataEncoder metadataEncoder, Clock clock) { - ForumListValidator validator = new ForumListValidator(clientHelper, + + ForumSharingValidator validator = new ForumSharingValidator(clientHelper, metadataEncoder, clock); - validationManager.registerMessageValidator( + messageQueueManager.registerMessageValidator( ForumSharingManagerImpl.CLIENT_ID, validator); + return validator; } @@ -78,15 +81,17 @@ public class ForumModule { ForumSharingManager provideForumSharingManager( LifecycleManager lifecycleManager, ContactManager contactManager, - ValidationManager validationManager, + MessageQueueManager messageQueueManager, ForumManager forumManager, ForumSharingManagerImpl forumSharingManager) { + lifecycleManager.registerClient(forumSharingManager); contactManager.registerAddContactHook(forumSharingManager); contactManager.registerRemoveContactHook(forumSharingManager); - validationManager.registerIncomingMessageHook( + messageQueueManager.registerIncomingMessageHook( ForumSharingManagerImpl.CLIENT_ID, forumSharingManager); forumManager.registerRemoveForumHook(forumSharingManager); + return forumSharingManager; } } diff --git a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java index 62501e8e5..c970571a9 100644 --- a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java +++ b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java @@ -1,74 +1,140 @@ package org.briarproject.forum; +import org.briarproject.api.Bytes; import org.briarproject.api.FormatException; import org.briarproject.api.clients.Client; import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.clients.MessageQueueManager; import org.briarproject.api.clients.PrivateGroupFactory; +import org.briarproject.api.clients.SessionId; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactManager.AddContactHook; import org.briarproject.api.contact.ContactManager.RemoveContactHook; import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfEntry; import org.briarproject.api.data.BdfList; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.data.MetadataParser; import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DbException; import org.briarproject.api.db.Metadata; +import org.briarproject.api.db.NoSuchMessageException; import org.briarproject.api.db.Transaction; +import org.briarproject.api.event.Event; import org.briarproject.api.forum.Forum; +import org.briarproject.api.forum.ForumInvitationMessage; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumSharingManager; +import org.briarproject.api.forum.InviteeAction; +import org.briarproject.api.forum.InviteeProtocolState; +import org.briarproject.api.forum.SharerAction; +import org.briarproject.api.forum.SharerProtocolState; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; -import org.briarproject.api.sync.ValidationManager.IncomingMessageHook; +import org.briarproject.api.sync.MessageStatus; import org.briarproject.api.system.Clock; +import org.briarproject.clients.BdfIncomingMessageHook; import org.briarproject.util.StringUtils; import java.io.IOException; +import java.security.SecureRandom; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; +import java.util.logging.Logger; import javax.inject.Inject; -import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.clients.ProtocolEngine.StateUpdate; +import static org.briarproject.api.forum.ForumConstants.CONTACT_ID; +import static org.briarproject.api.forum.ForumConstants.FORUM_ID; +import static org.briarproject.api.forum.ForumConstants.FORUM_NAME; +import static org.briarproject.api.forum.ForumConstants.FORUM_SALT; +import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH; +import static org.briarproject.api.forum.ForumConstants.GROUP_ID; +import static org.briarproject.api.forum.ForumConstants.INVITATION_MSG; +import static org.briarproject.api.forum.ForumConstants.IS_SHARER; +import static org.briarproject.api.forum.ForumConstants.LOCAL; +import static org.briarproject.api.forum.ForumConstants.READ; +import static org.briarproject.api.forum.ForumConstants.SESSION_ID; +import static org.briarproject.api.forum.ForumConstants.SHARED_BY_US; +import static org.briarproject.api.forum.ForumConstants.SHARED_WITH_US; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE; +import static org.briarproject.api.forum.ForumConstants.STATE; +import static org.briarproject.api.forum.ForumConstants.STORAGE_ID; +import static org.briarproject.api.forum.ForumConstants.TASK; +import static org.briarproject.api.forum.ForumConstants.TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US; +import static org.briarproject.api.forum.ForumConstants.TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US; +import static org.briarproject.api.forum.ForumConstants.TASK_ADD_SHARED_FORUM; +import static org.briarproject.api.forum.ForumConstants.TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US; +import static org.briarproject.api.forum.ForumConstants.TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US; +import static org.briarproject.api.forum.ForumConstants.TASK_SHARE_FORUM; +import static org.briarproject.api.forum.ForumConstants.TASK_UNSHARE_FORUM_SHARED_BY_US; +import static org.briarproject.api.forum.ForumConstants.TASK_UNSHARE_FORUM_SHARED_WITH_US; +import static org.briarproject.api.forum.ForumConstants.TIME; +import static org.briarproject.api.forum.ForumConstants.TO_BE_SHARED_BY_US; +import static org.briarproject.api.forum.ForumConstants.TYPE; import static org.briarproject.api.forum.ForumManager.RemoveForumHook; +import static org.briarproject.api.forum.InviteeProtocolState.AWAIT_INVITATION; +import static org.briarproject.api.forum.InviteeProtocolState.AWAIT_LOCAL_RESPONSE; +import static org.briarproject.api.forum.SharerProtocolState.PREPARE_INVITATION; -class ForumSharingManagerImpl implements ForumSharingManager, Client, - AddContactHook, RemoveContactHook, IncomingMessageHook, - RemoveForumHook { +class ForumSharingManagerImpl extends BdfIncomingMessageHook + implements ForumSharingManager, Client, RemoveForumHook, + AddContactHook, RemoveContactHook { static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString( "cd11a5d04dccd9e2931d6fc3df456313" + "63bb3e9d9d0e9405fccdb051f41f5449")); + private static final Logger LOG = + Logger.getLogger(ForumSharingManagerImpl.class.getName()); + private final DatabaseComponent db; private final ForumManager forumManager; - private final ClientHelper clientHelper; + private final MessageQueueManager messageQueueManager; + private final MetadataEncoder metadataEncoder; + private final SecureRandom random; private final PrivateGroupFactory privateGroupFactory; private final Clock clock; + private final Group localGroup; @Inject ForumSharingManagerImpl(DatabaseComponent db, ForumManager forumManager, - ClientHelper clientHelper, PrivateGroupFactory privateGroupFactory, + MessageQueueManager messageQueueManager, ClientHelper clientHelper, + MetadataParser metadataParser, MetadataEncoder metadataEncoder, + SecureRandom random, PrivateGroupFactory privateGroupFactory, Clock clock) { + super(clientHelper, metadataParser); this.db = db; this.forumManager = forumManager; - this.clientHelper = clientHelper; + this.messageQueueManager = messageQueueManager; + this.metadataEncoder = metadataEncoder; + this.random = random; this.privateGroupFactory = privateGroupFactory; this.clock = clock; + localGroup = privateGroupFactory.createLocalGroup(getClientId()); } @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); } @@ -85,7 +151,10 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client, db.setVisibleToContact(txn, c.getId(), g.getId(), true); // Attach the contact ID to the group BdfDictionary meta = new BdfDictionary(); - meta.put("contactId", c.getId().getInt()); + 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); @@ -94,17 +163,88 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client, @Override public void removingContact(Transaction txn, Contact c) throws DbException { + // clean up session states with that contact from localGroup + Long id = (long) c.getId().getInt(); + try { + Map map = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId()); + for (Map.Entry entry : map.entrySet()) { + BdfDictionary d = entry.getValue(); + if (id.equals(d.getLong(CONTACT_ID))) { + 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 void incomingMessage(Transaction txn, Message m, Metadata meta) - throws DbException { - try { - ContactId contactId = getContactId(txn, m.getGroupId()); - setForumVisibility(txn, contactId, getVisibleForums(txn, m)); - } catch (FormatException e) { - throw new DbException(e); + protected void incomingMessage(Transaction txn, Message m, BdfList body, + BdfDictionary msg) throws DbException, FormatException { + + SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID)); + long type = msg.getLong(TYPE); + if (type == 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; + } + try { + // check if we already have a state with that sessionId + if (stateExists) throw new FormatException(); + + // check if forum can be shared + Forum f = forumManager.createForum(msg.getString(FORUM_NAME), + msg.getRaw(FORUM_SALT)); + ContactId contactId = getContactId(txn, m.getGroupId()); + Contact contact = db.getContact(txn, contactId); + if (!canBeShared(txn, f.getId(), contact)) + throw new FormatException(); + + // initialize state and process invitation + BdfDictionary state = + initializeInviteeState(txn, contactId, msg); + InviteeEngine engine = new InviteeEngine(); + processStateUpdate(txn, m.getId(), + engine.onMessageReceived(state, msg)); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + deleteMessage(txn, m.getId()); + } + } else if (type == SHARE_MSG_TYPE_ACCEPT || + type == SHARE_MSG_TYPE_DECLINE) { + // we are a sharer who just received a response + BdfDictionary state = getSessionState(txn, sessionId, true); + SharerEngine engine = new SharerEngine(); + processStateUpdate(txn, m.getId(), + engine.onMessageReceived(state, msg)); + } else if (type == SHARE_MSG_TYPE_LEAVE || + type == SHARE_MSG_TYPE_ABORT) { + // we don't know who we are, so figure it out + BdfDictionary state = getSessionState(txn, sessionId, true); + if (state.getBoolean(IS_SHARER)) { + // we are a sharer and the invitee wants to leave or abort + SharerEngine engine = new SharerEngine(); + processStateUpdate(txn, m.getId(), + engine.onMessageReceived(state, msg)); + } else { + // we are an invitee and the sharer wants to leave or abort + InviteeEngine engine = new InviteeEngine(); + processStateUpdate(txn, m.getId(), + engine.onMessageReceived(state, msg)); + } + } else { + // message has passed validator, so that should never happen + throw new RuntimeException("Illegal Forum Sharing Message"); } } @@ -113,11 +253,138 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client, return CLIENT_ID; } + @Override + public void sendForumInvitation(GroupId groupId, ContactId contactId, + String msg) throws DbException { + + Transaction txn = db.startTransaction(false); + try { + // initialize local state for sharer + Forum f = forumManager.getForum(txn, groupId); + BdfDictionary localState = initializeSharerState(txn, f, contactId); + + // define action + BdfDictionary localAction = new BdfDictionary(); + localAction.put(TYPE, SHARE_MSG_TYPE_INVITATION); + if (!StringUtils.isNullOrEmpty(msg)) { + localAction.put(INVITATION_MSG, msg); + } + + // start engine and process its state update + SharerEngine engine = new SharerEngine(); + processStateUpdate(txn, null, + engine.onLocalAction(localState, localAction)); + + txn.setComplete(); + } catch (FormatException e) { + throw new DbException(); + } finally { + db.endTransaction(txn); + } + } + + @Override + public void respondToInvitation(Forum f, boolean accept) + throws DbException { + + Transaction txn = db.startTransaction(false); + try { + // find session state based on forum + BdfDictionary localState = getSessionStateForResponse(txn, f); + + // define action + BdfDictionary localAction = new BdfDictionary(); + if (accept) { + localAction.put(TYPE, SHARE_MSG_TYPE_ACCEPT); + } else { + localAction.put(TYPE, SHARE_MSG_TYPE_DECLINE); + } + + // start engine and process its state update + InviteeEngine engine = new InviteeEngine(); + processStateUpdate(txn, null, + engine.onLocalAction(localState, localAction)); + + txn.setComplete(); + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + } + + @Override + public Collection getForumInvitationMessages( + ContactId contactId) throws DbException { + + Transaction txn = db.startTransaction(false); + try { + Contact contact = db.getContact(txn, contactId); + Group group = getContactGroup(contact); + + Collection list = + new ArrayList(); + Map map = clientHelper + .getMessageMetadataAsDictionary(txn, group.getId()); + for (Map.Entry m : map.entrySet()) { + BdfDictionary msg = m.getValue(); + try { + if (msg.getLong(TYPE) != SHARE_MSG_TYPE_INVITATION) + continue; + + MessageStatus status = + db.getMessageStatus(txn, contactId, m.getKey()); + SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID)); + String name = msg.getString(FORUM_NAME); + String message = msg.getOptionalString(INVITATION_MSG); + long time = msg.getLong(TIME); + boolean local = msg.getBoolean(LOCAL); + boolean read = msg.getBoolean(READ, false); + boolean available = false; + if (!local) { + // figure out whether the forum is still available + BdfDictionary sessionState = + getSessionState(txn, sessionId, true); + InviteeProtocolState state = InviteeProtocolState + .fromValue( + sessionState.getLong(STATE).intValue()); + available = state == AWAIT_LOCAL_RESPONSE; + } + ForumInvitationMessage im = + new ForumInvitationMessage(m.getKey(), sessionId, + contactId, name, message, available, time, + local, status.isSent(), status.isSeen(), + read); + list.add(im); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + txn.setComplete(); + return list; + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + } + @Override public void removingForum(Transaction txn, Forum f) throws DbException { try { - for (Contact c : db.getContacts(txn)) - removeFromList(txn, getContactGroup(c).getId(), f); + for (Contact c : db.getContacts(txn)) { + GroupId g = getContactGroup(c).getId(); + if (removeFromList(txn, g, TO_BE_SHARED_BY_US, f)) { + leaveForum(txn, c.getId(), f); + } + if (removeFromList(txn, g, SHARED_BY_US, f)) { + leaveForum(txn, c.getId(), f); + } + if (removeFromList(txn, g, SHARED_WITH_US, f)) { + leaveForum(txn, c.getId(), f); + } + } } catch (IOException e) { throw new DbException(e); } @@ -135,16 +402,11 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client, // Get all forums shared by contacts for (Contact c : db.getContacts(txn)) { Group g = getContactGroup(c); - // Find the latest update version - LatestUpdate latest = findLatest(txn, g.getId(), false); - if (latest != null) { - // Retrieve and parse the latest update - BdfList message = clientHelper.getMessageAsList(txn, - latest.messageId); - for (Forum f : parseForumList(message)) { - if (!subscribed.contains(f.getGroup())) - available.add(f); - } + List forums = + getForumList(txn, g.getId(), SHARED_WITH_US); + for (Forum f : forums) { + if (!subscribed.contains(f.getGroup())) + available.add(f); } } txn.setComplete(); @@ -164,7 +426,8 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client, Transaction txn = db.startTransaction(true); try { for (Contact c : db.getContacts(txn)) { - if (listContains(txn, getContactGroup(c).getId(), g, false)) + GroupId contactGroup = getContactGroup(c).getId(); + if (listContains(txn, contactGroup, g, SHARED_WITH_US)) subscribers.add(c); } txn.setComplete(); @@ -184,7 +447,8 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client, Transaction txn = db.startTransaction(true); try { for (Contact c : db.getContacts(txn)) { - if (listContains(txn, getContactGroup(c).getId(), g, true)) + GroupId contactGroup = getContactGroup(c).getId(); + if (listContains(txn, contactGroup, g, SHARED_BY_US)) shared.add(c.getId()); } txn.setComplete(); @@ -198,178 +462,417 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client, } @Override - public void setSharedWith(GroupId g, Collection shared) - throws DbException { + public boolean canBeShared(GroupId g, Contact c) throws DbException { + boolean canBeShared; + Transaction txn = db.startTransaction(true); try { - Transaction txn = db.startTransaction(false); - try { - // Retrieve the forum - Forum f = parseForum(db.getGroup(txn, g)); - // Update the list shared with each contact - shared = new HashSet(shared); - for (Contact c : db.getContacts(txn)) { - Group cg = getContactGroup(c); - if (shared.contains(c.getId())) { - if (addToList(txn, cg.getId(), f)) { - if (listContains(txn, cg.getId(), g, false)) - db.setVisibleToContact(txn, c.getId(), g, true); - } - } else { - removeFromList(txn, cg.getId(), f); - db.setVisibleToContact(txn, c.getId(), g, false); - } - } - txn.setComplete(); - } finally { - db.endTransaction(txn); - } + canBeShared = canBeShared(txn, g, c); + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + return canBeShared; + } + + private 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); } } + private BdfDictionary initializeSharerState(Transaction txn, Forum 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[FORUM_SALT_LENGTH]); + random.nextBytes(salt.getBytes()); + Message m = clientHelper.createMessage(localGroup.getId(), now, + BdfList.of(salt)); + MessageId sessionId = m.getId(); + + BdfDictionary d = new BdfDictionary(); + d.put(SESSION_ID, sessionId); + d.put(STORAGE_ID, sessionId); + d.put(GROUP_ID, group.getId()); + d.put(IS_SHARER, true); + d.put(STATE, PREPARE_INVITATION.getValue()); + d.put(CONTACT_ID, contactId.getInt()); + d.put(FORUM_ID, f.getId()); + d.put(FORUM_NAME, f.getName()); + d.put(FORUM_SALT, f.getSalt()); + + // save local state to database + clientHelper.addLocalMessage(txn, m, getClientId(), d, false); + + return d; + } + + private BdfDictionary initializeInviteeState(Transaction txn, + ContactId contactId, BdfDictionary msg) + throws FormatException, DbException { + + Contact c = db.getContact(txn, contactId); + Group group = getContactGroup(c); + String name = msg.getString(FORUM_NAME); + byte[] salt = msg.getRaw(FORUM_SALT); + Forum f = forumManager.createForum(name, salt); + + // create local message to keep engine state + long now = clock.currentTimeMillis(); + Bytes mSalt = new Bytes(new byte[FORUM_SALT_LENGTH]); + random.nextBytes(mSalt.getBytes()); + Message m = clientHelper.createMessage(localGroup.getId(), now, + BdfList.of(mSalt)); + + BdfDictionary d = new BdfDictionary(); + d.put(SESSION_ID, msg.getRaw(SESSION_ID)); + d.put(STORAGE_ID, m.getId()); + d.put(GROUP_ID, group.getId()); + d.put(IS_SHARER, false); + d.put(STATE, AWAIT_INVITATION.getValue()); + d.put(CONTACT_ID, contactId.getInt()); + d.put(FORUM_ID, f.getId()); + d.put(FORUM_NAME, name); + d.put(FORUM_SALT, salt); + + // save local state to database + clientHelper.addLocalMessage(txn, m, getClientId(), d, false); + + return d; + } + + private BdfDictionary getSessionState(Transaction txn, SessionId sessionId, + boolean warn) throws DbException, FormatException { + + try { + // we should be able to get the sharer state directly from sessionId + return clientHelper.getMessageMetadataAsDictionary(txn, sessionId); + } catch (NoSuchMessageException e) { + // State not found directly, so iterate over all states + // to find state for invitee + Map map = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId()); + for (Map.Entry m : map.entrySet()) { + BdfDictionary state = m.getValue(); + if (Arrays.equals(state.getRaw(SESSION_ID), + sessionId.getBytes())) { + return state; + } + } + if (warn && LOG.isLoggable(WARNING)) { + LOG.warning( + "No session state found for message with session ID " + + Arrays.hashCode(sessionId.getBytes())); + } + throw new FormatException(); + } + } + + private BdfDictionary getSessionStateForResponse(Transaction txn, Forum f) + throws DbException, FormatException { + + Map map = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId()); + for (Map.Entry m : map.entrySet()) { + BdfDictionary d = m.getValue(); + try { + InviteeProtocolState state = InviteeProtocolState + .fromValue(d.getLong(STATE).intValue()); + if (state == AWAIT_LOCAL_RESPONSE) { + byte[] id = d.getRaw(FORUM_ID); + if (Arrays.equals(f.getId().getBytes(), id)) { + // Note that there should always be only one session + // in this state for the same forum + return d; + } + } + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + throw new DbException(); + } + + private BdfDictionary getSessionStateForLeaving(Transaction txn, Forum f, + ContactId c) throws DbException, FormatException { + + Map map = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId()); + for (Map.Entry m : map.entrySet()) { + BdfDictionary d = m.getValue(); + try { + // check that this session is with the right contact + if (c.getInt() != d.getLong(CONTACT_ID)) continue; + // check that a forum get be left in current session + int intState = d.getLong(STATE).intValue(); + if (d.getBoolean(IS_SHARER)) { + SharerProtocolState state = + SharerProtocolState.fromValue(intState); + if (state.next(SharerAction.LOCAL_LEAVE) == + SharerProtocolState.ERROR) continue; + } else { + InviteeProtocolState state = InviteeProtocolState + .fromValue(intState); + if (state.next(InviteeAction.LOCAL_LEAVE) == + InviteeProtocolState.ERROR) continue; + } + // check that this state actually concerns this forum + String name = d.getString(FORUM_NAME); + byte[] salt = d.getRaw(FORUM_SALT); + if (name.equals(f.getName()) && + Arrays.equals(salt, f.getSalt())) { + // TODO what happens when there is more than one invitation? + return d; + } + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + throw new FormatException(); + } + + private void processStateUpdate(Transaction txn, MessageId messageId, + StateUpdate result) + throws DbException, FormatException { + + // perform actions based on new local state + performTasks(txn, result.localState); + + // save new local state + MessageId storageId = + new MessageId(result.localState.getRaw(STORAGE_ID)); + clientHelper.mergeMessageMetadata(txn, storageId, result.localState); + + // send messages + for (BdfDictionary d : result.toSend) { + sendMessage(txn, d); + } + + // 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 performTasks(Transaction txn, BdfDictionary localState) + throws FormatException, DbException { + + if (!localState.containsKey(TASK)) return; + + // remember task and remove it from localState + long task = localState.getLong(TASK); + localState.put(TASK, BdfDictionary.NULL_VALUE); + + // get group ID for later + GroupId groupId = new GroupId(localState.getRaw(GROUP_ID)); + // get contact ID for later + ContactId contactId = + new ContactId(localState.getLong(CONTACT_ID).intValue()); + + // get forum for later + String name = localState.getString(FORUM_NAME); + byte[] salt = localState.getRaw(FORUM_SALT); + Forum f = forumManager.createForum(name, salt); + + // perform tasks + if (task == TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US) { + addToList(txn, groupId, SHARED_WITH_US, f); + } + else if (task == TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US) { + removeFromList(txn, groupId, SHARED_WITH_US, f); + } + else if (task == TASK_ADD_SHARED_FORUM) { + db.addGroup(txn, f.getGroup()); + db.setVisibleToContact(txn, contactId, f.getId(), true); + } + else if (task == TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US) { + addToList(txn, groupId, TO_BE_SHARED_BY_US, f); + } + else if (task == TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US) { + removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f); + } + else if (task == TASK_SHARE_FORUM) { + db.setVisibleToContact(txn, contactId, f.getId(), true); + removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f); + addToList(txn, groupId, SHARED_BY_US, f); + } + else if (task == TASK_UNSHARE_FORUM_SHARED_BY_US) { + db.setVisibleToContact(txn, contactId, f.getId(), false); + removeFromList(txn, groupId, SHARED_BY_US, f); + } else if (task == TASK_UNSHARE_FORUM_SHARED_WITH_US) { + db.setVisibleToContact(txn, contactId, f.getId(), false); + removeFromList(txn, groupId, SHARED_WITH_US, f); + } + } + + private void sendMessage(Transaction txn, BdfDictionary m) + throws FormatException, DbException { + + BdfList list = encodeMessage(m); + byte[] body = clientHelper.toByteArray(list); + GroupId groupId = new GroupId(m.getRaw(GROUP_ID)); + Group group = db.getGroup(txn, groupId); + long timestamp = clock.currentTimeMillis(); + + // add message itself as metadata + m.put(LOCAL, true); + m.put(TIME, timestamp); + Metadata meta = metadataEncoder.encode(m); + + messageQueueManager + .sendMessage(txn, group, timestamp, body, meta); + } + + private BdfList encodeMessage(BdfDictionary m) throws FormatException { + long type = m.getLong(TYPE); + + BdfList list; + if (type == SHARE_MSG_TYPE_INVITATION) { + list = BdfList.of(type, + m.getRaw(SESSION_ID), + m.getString(FORUM_NAME), + m.getRaw(FORUM_SALT) + ); + String msg = m.getOptionalString(INVITATION_MSG); + if (msg != null) list.add(msg); + } else if (type == SHARE_MSG_TYPE_ACCEPT) { + list = BdfList.of(type, m.getRaw(SESSION_ID)); + } else if (type == SHARE_MSG_TYPE_DECLINE) { + list = BdfList.of(type, m.getRaw(SESSION_ID)); + } else if (type == SHARE_MSG_TYPE_LEAVE) { + list = BdfList.of(type, m.getRaw(SESSION_ID)); + } else if (type == SHARE_MSG_TYPE_ABORT) { + list = BdfList.of(type, m.getRaw(SESSION_ID)); + } else { + throw new FormatException(); + } + return list; + } + private Group getContactGroup(Contact c) { return privateGroupFactory.createPrivateGroup(CLIENT_ID, c); } - private LatestUpdate findLatest(Transaction txn, GroupId g, boolean local) + private ContactId getContactId(Transaction txn, GroupId contactGroupId) throws DbException, FormatException { - LatestUpdate latest = null; - Map metadata = - clientHelper.getMessageMetadataAsDictionary(txn, g); - for (Entry e : metadata.entrySet()) { - BdfDictionary meta = e.getValue(); - if (meta.getBoolean("local") != local) continue; - long version = meta.getLong("version"); - if (latest == null || version > latest.version) - latest = new LatestUpdate(e.getKey(), version); - } - return latest; + BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn, + contactGroupId); + return new ContactId(meta.getLong(CONTACT_ID).intValue()); } - private List parseForumList(BdfList message) throws FormatException { - // Version, forum list - BdfList forumList = message.getList(1); - List forums = new ArrayList(forumList.size()); - for (int i = 0; i < forumList.size(); i++) { - // Name, salt - BdfList forum = forumList.getList(i); + private void leaveForum(Transaction txn, ContactId c, Forum f) + throws DbException, FormatException { + + BdfDictionary state = getSessionStateForLeaving(txn, f, c); + BdfDictionary action = new BdfDictionary(); + action.put(TYPE, SHARE_MSG_TYPE_LEAVE); + if (state.getBoolean(IS_SHARER)) { + SharerEngine engine = new SharerEngine(); + processStateUpdate(txn, null, + engine.onLocalAction(state, action)); + } else { + InviteeEngine engine = new InviteeEngine(); + processStateUpdate(txn, null, + engine.onLocalAction(state, action)); + } + } + + private boolean listContains(Transaction txn, GroupId contactGroup, + GroupId forum, String key) throws DbException, FormatException { + + List list = getForumList(txn, contactGroup, key); + for (Forum f : list) { + if (f.getId().equals(forum)) return true; + } + return false; + } + + private boolean addToList(Transaction txn, GroupId groupId, String key, + Forum f) throws DbException, FormatException { + + List forums = getForumList(txn, groupId, key); + if (forums.contains(f)) return false; + forums.add(f); + storeForumList(txn, groupId, key, forums); + return true; + } + + private boolean removeFromList(Transaction txn, GroupId groupId, String key, + Forum f) throws DbException, FormatException { + + List forums = getForumList(txn, groupId, key); + if (forums.remove(f)) { + storeForumList(txn, groupId, key, forums); + return true; + } + return false; + } + + private List getForumList(Transaction txn, GroupId groupId, + String key) throws DbException, FormatException { + + BdfDictionary metadata = + clientHelper.getGroupMetadataAsDictionary(txn, groupId); + BdfList list = metadata.getList(key); + + return parseForumList(list); + } + + private void storeForumList(Transaction txn, GroupId groupId, String key, + List forums) throws DbException, FormatException { + + BdfList list = encodeForumList(forums); + BdfDictionary metadata = BdfDictionary.of( + new BdfEntry(key, list) + ); + clientHelper.mergeGroupMetadata(txn, groupId, metadata); + } + + private BdfList encodeForumList(List forums) { + BdfList forumList = new BdfList(); + for (Forum f : forums) + forumList.add(BdfList.of(f.getName(), f.getSalt())); + return forumList; + } + + private List parseForumList(BdfList list) throws FormatException { + List forums = new ArrayList(list.size()); + for (int i = 0; i < list.size(); i++) { + BdfList forum = list.getList(i); forums.add(forumManager .createForum(forum.getString(0), forum.getRaw(1))); } return forums; } - private void storeMessage(Transaction txn, GroupId g, List forums, - long version) throws DbException { - try { - BdfList body = encodeForumList(forums, version); - long now = clock.currentTimeMillis(); - Message m = clientHelper.createMessage(g, now, body); - BdfDictionary meta = new BdfDictionary(); - meta.put("version", version); - meta.put("local", true); - clientHelper.addLocalMessage(txn, m, CLIENT_ID, meta, true); - } catch (FormatException e) { - throw new RuntimeException(e); - } + 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); } - private BdfList encodeForumList(List forums, long version) { - BdfList forumList = new BdfList(); - for (Forum f : forums) - forumList.add(BdfList.of(f.getName(), f.getSalt())); - return BdfList.of(version, forumList); - } - - private ContactId getContactId(Transaction txn, GroupId contactGroupId) - throws DbException, FormatException { - BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn, - contactGroupId); - return new ContactId(meta.getLong("contactId").intValue()); - } - - private Set getVisibleForums(Transaction txn, - Message remoteUpdate) throws DbException, FormatException { - // Get the latest local update - LatestUpdate local = findLatest(txn, remoteUpdate.getGroupId(), true); - // If there's no local update, no forums are visible - if (local == null) return Collections.emptySet(); - // Intersect the sets of shared forums - BdfList localMessage = clientHelper.getMessageAsList(txn, - local.messageId); - Set shared = new HashSet(parseForumList(localMessage)); - byte[] raw = remoteUpdate.getRaw(); - BdfList remoteMessage = clientHelper.toList(raw, MESSAGE_HEADER_LENGTH, - raw.length - MESSAGE_HEADER_LENGTH); - shared.retainAll(parseForumList(remoteMessage)); - // Forums in the intersection should be visible - Set visible = new HashSet(shared.size()); - for (Forum f : shared) visible.add(f.getId()); - return visible; - } - - private void setForumVisibility(Transaction txn, ContactId c, - Set visible) throws DbException { - for (Group g : db.getGroups(txn, forumManager.getClientId())) { - boolean isVisible = db.isVisibleToContact(txn, c, g.getId()); - boolean shouldBeVisible = visible.contains(g.getId()); - if (isVisible && !shouldBeVisible) - db.setVisibleToContact(txn, c, g.getId(), false); - else if (!isVisible && shouldBeVisible) - db.setVisibleToContact(txn, c, g.getId(), true); - } - } - - private Forum parseForum(Group g) throws FormatException { - byte[] descriptor = g.getDescriptor(); - // Name, salt - BdfList forum = clientHelper.toList(descriptor, 0, descriptor.length); - return new Forum(g, forum.getString(0), forum.getRaw(1)); - } - - private boolean listContains(Transaction txn, GroupId g, GroupId forum, - boolean local) throws DbException, FormatException { - LatestUpdate latest = findLatest(txn, g, local); - if (latest == null) return false; - BdfList message = clientHelper.getMessageAsList(txn, latest.messageId); - List list = parseForumList(message); - for (Forum f : list) if (f.getId().equals(forum)) return true; - return false; - } - - private boolean addToList(Transaction txn, GroupId g, Forum f) - throws DbException, FormatException { - LatestUpdate latest = findLatest(txn, g, true); - if (latest == null) { - storeMessage(txn, g, Collections.singletonList(f), 0); - return true; - } - BdfList message = clientHelper.getMessageAsList(txn, latest.messageId); - List list = parseForumList(message); - if (list.contains(f)) return false; - list.add(f); - storeMessage(txn, g, list, latest.version + 1); - return true; - } - - private void removeFromList(Transaction txn, GroupId g, Forum f) - throws DbException, FormatException { - LatestUpdate latest = findLatest(txn, g, true); - if (latest == null) return; - BdfList message = clientHelper.getMessageAsList(txn, latest.messageId); - List list = parseForumList(message); - if (list.remove(f)) storeMessage(txn, g, list, latest.version + 1); - } - - private static class LatestUpdate { - - private final MessageId messageId; - private final long version; - - private LatestUpdate(MessageId messageId, long version) { - this.messageId = messageId; - this.version = version; - } - } } diff --git a/briar-core/src/org/briarproject/forum/ForumSharingValidator.java b/briar-core/src/org/briarproject/forum/ForumSharingValidator.java new file mode 100644 index 000000000..c204623a2 --- /dev/null +++ b/briar-core/src/org/briarproject/forum/ForumSharingValidator.java @@ -0,0 +1,81 @@ +package org.briarproject.forum; + +import org.briarproject.api.FormatException; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.clients.SessionId; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.Message; +import org.briarproject.api.system.Clock; +import org.briarproject.clients.BdfMessageValidator; + +import static org.briarproject.api.forum.ForumConstants.FORUM_NAME; +import static org.briarproject.api.forum.ForumConstants.FORUM_SALT; +import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH; +import static org.briarproject.api.forum.ForumConstants.GROUP_ID; +import static org.briarproject.api.forum.ForumConstants.INVITATION_MSG; +import static org.briarproject.api.forum.ForumConstants.LOCAL; +import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH; +import static org.briarproject.api.forum.ForumConstants.SESSION_ID; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE; +import static org.briarproject.api.forum.ForumConstants.TIME; +import static org.briarproject.api.forum.ForumConstants.TYPE; +import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; + +class ForumSharingValidator extends BdfMessageValidator { + + ForumSharingValidator(ClientHelper clientHelper, + MetadataEncoder metadataEncoder, Clock clock) { + super(clientHelper, metadataEncoder, clock); + } + + @Override + protected BdfDictionary 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_MESSAGE_BODY_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(GROUP_ID, m.getGroupId()); + d.put(LOCAL, false); + d.put(TIME, m.getTimestamp()); + return d; + } +} diff --git a/briar-core/src/org/briarproject/forum/InviteeEngine.java b/briar-core/src/org/briarproject/forum/InviteeEngine.java new file mode 100644 index 000000000..f208204db --- /dev/null +++ b/briar-core/src/org/briarproject/forum/InviteeEngine.java @@ -0,0 +1,265 @@ +package org.briarproject.forum; + +import org.briarproject.api.FormatException; +import org.briarproject.api.clients.ProtocolEngine; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfEntry; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.ForumInvitationReceivedEvent; +import org.briarproject.api.forum.Forum; +import org.briarproject.api.forum.InviteeAction; +import org.briarproject.api.forum.InviteeProtocolState; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.forum.ForumConstants.CONTACT_ID; +import static org.briarproject.api.forum.ForumConstants.FORUM_NAME; +import static org.briarproject.api.forum.ForumConstants.FORUM_SALT; +import static org.briarproject.api.forum.ForumConstants.GROUP_ID; +import static org.briarproject.api.forum.ForumConstants.SESSION_ID; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE; +import static org.briarproject.api.forum.ForumConstants.STATE; +import static org.briarproject.api.forum.ForumConstants.TASK; +import static org.briarproject.api.forum.ForumConstants.TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US; +import static org.briarproject.api.forum.ForumConstants.TASK_ADD_SHARED_FORUM; +import static org.briarproject.api.forum.ForumConstants.TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US; +import static org.briarproject.api.forum.ForumConstants.TASK_UNSHARE_FORUM_SHARED_WITH_US; +import static org.briarproject.api.forum.ForumConstants.TYPE; +import static org.briarproject.api.forum.InviteeAction.LOCAL_ABORT; +import static org.briarproject.api.forum.InviteeAction.LOCAL_ACCEPT; +import static org.briarproject.api.forum.InviteeAction.LOCAL_DECLINE; +import static org.briarproject.api.forum.InviteeAction.LOCAL_LEAVE; +import static org.briarproject.api.forum.InviteeAction.REMOTE_INVITATION; +import static org.briarproject.api.forum.InviteeAction.REMOTE_LEAVE; +import static org.briarproject.api.forum.InviteeProtocolState.ERROR; +import static org.briarproject.api.forum.InviteeProtocolState.FINISHED; +import static org.briarproject.api.forum.InviteeProtocolState.LEFT; + +public class InviteeEngine + implements ProtocolEngine { + + private static final Logger LOG = + Logger.getLogger(SharerEngine.class.getName()); + + @Override + public StateUpdate onLocalAction( + BdfDictionary localState, BdfDictionary localAction) { + + try { + InviteeProtocolState currentState = + getState(localState.getLong(STATE)); + long type = localAction.getLong(TYPE); + InviteeAction action = InviteeAction.getLocal(type); + InviteeProtocolState nextState = currentState.next(action); + localState.put(STATE, nextState.getValue()); + + if (action == LOCAL_ABORT && currentState != ERROR) { + return abortSession(currentState, localState); + } + + if (nextState == ERROR) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("Error: Invalid action in state " + + currentState.name()); + } + return noUpdate(localState, true); + } + List messages; + List events = Collections.emptyList(); + + if (action == LOCAL_ACCEPT || action == LOCAL_DECLINE) { + BdfDictionary msg = BdfDictionary.of( + new BdfEntry(SESSION_ID, localState.getRaw(SESSION_ID)), + new BdfEntry(GROUP_ID, localState.getRaw(GROUP_ID)) + ); + if (action == LOCAL_ACCEPT) { + localState.put(TASK, TASK_ADD_SHARED_FORUM); + msg.put(TYPE, SHARE_MSG_TYPE_ACCEPT); + } else { + localState.put(TASK, + TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US); + msg.put(TYPE, SHARE_MSG_TYPE_DECLINE); + } + messages = Collections.singletonList(msg); + logLocalAction(currentState, localState, msg); + } + else if (action == LOCAL_LEAVE) { + BdfDictionary msg = new BdfDictionary(); + msg.put(TYPE, SHARE_MSG_TYPE_LEAVE); + msg.put(SESSION_ID, localState.getRaw(SESSION_ID)); + msg.put(GROUP_ID, localState.getRaw(GROUP_ID)); + messages = Collections.singletonList(msg); + logLocalAction(currentState, localState, msg); + } + else { + throw new IllegalArgumentException("Unknown Local Action"); + } + return new StateUpdate(false, + false, localState, messages, events); + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public StateUpdate onMessageReceived( + BdfDictionary localState, BdfDictionary msg) { + + try { + InviteeProtocolState currentState = + getState(localState.getLong(STATE)); + long type = msg.getLong(TYPE); + InviteeAction action = InviteeAction.getRemote(type); + InviteeProtocolState nextState = currentState.next(action); + localState.put(STATE, nextState.getValue()); + + logMessageReceived(currentState, nextState, type, msg); + + if (nextState == ERROR) { + if (currentState != ERROR) { + return abortSession(currentState, localState); + } else { + return noUpdate(localState, true); + } + } + + List messages = Collections.emptyList(); + List events = Collections.emptyList(); + boolean deleteMsg = false; + + if (currentState == LEFT) { + // ignore and delete messages coming in while in that state + deleteMsg = true; + } + // the sharer left the forum she had shared with us + else if (action == REMOTE_LEAVE && currentState == FINISHED) { + localState.put(TASK, TASK_UNSHARE_FORUM_SHARED_WITH_US); + } + else if (currentState == FINISHED) { + // ignore and delete messages coming in while in that state + // note that LEAVE is possible, but was handled above + deleteMsg = true; + } + // the sharer left the forum before we couldn't even respond + else if (action == REMOTE_LEAVE) { + localState.put(TASK, TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US); + } + // we have just received our invitation + else if (action == REMOTE_INVITATION) { + localState.put(TASK, TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US); + // TODO how to get the proper group here? + Forum forum = new Forum(null, localState.getString(FORUM_NAME), + localState.getRaw(FORUM_SALT)); + ContactId contactId = new ContactId( + localState.getLong(CONTACT_ID).intValue()); + Event event = new ForumInvitationReceivedEvent(forum, contactId); + events = Collections.singletonList(event); + } + else { + throw new IllegalArgumentException("Bad state"); + } + return new StateUpdate(deleteMsg, + false, localState, messages, events); + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + private void logLocalAction(InviteeProtocolState state, + BdfDictionary localState, BdfDictionary msg) { + + if (!LOG.isLoggable(INFO)) return; + + String a = "response"; + if (msg.getLong(TYPE, -1L) == SHARE_MSG_TYPE_LEAVE) a = "leave"; + + try { + LOG.info("Sending " + a + " in state " + state.name() + + " with session ID " + + Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " + + Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " + + "Moving on to state " + + getState(localState.getLong(STATE)).name() + ); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + + private void logMessageReceived(InviteeProtocolState currentState, + InviteeProtocolState nextState, long type, BdfDictionary msg) { + if (!LOG.isLoggable(INFO)) return; + + try { + String t = "unknown"; + if (type == SHARE_MSG_TYPE_INVITATION) t = "INVITE"; + else if (type == SHARE_MSG_TYPE_LEAVE) t = "LEAVE"; + else if (type == SHARE_MSG_TYPE_ABORT) t = "ABORT"; + + LOG.info("Received " + t + " in state " + currentState.name() + + " with session ID " + + Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " + + Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " + + "Moving on to state " + nextState.name() + ); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + + @Override + public StateUpdate onMessageDelivered( + BdfDictionary localState, BdfDictionary delivered) { + try { + return noUpdate(localState, false); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + return null; + } + } + + private InviteeProtocolState getState(Long state) { + return InviteeProtocolState.fromValue(state.intValue()); + } + + private StateUpdate abortSession( + InviteeProtocolState currentState, BdfDictionary localState) + throws FormatException { + + if (LOG.isLoggable(WARNING)) { + LOG.warning("Aborting protocol session " + + Arrays.hashCode(localState.getRaw(SESSION_ID)) + + " in state " + currentState.name()); + } + + localState.put(STATE, ERROR.getValue()); + BdfDictionary msg = new BdfDictionary(); + msg.put(TYPE, SHARE_MSG_TYPE_ABORT); + msg.put(SESSION_ID, localState.getRaw(SESSION_ID)); + msg.put(GROUP_ID, localState.getRaw(GROUP_ID)); + List messages = Collections.singletonList(msg); + + List events = Collections.emptyList(); + + return new StateUpdate(false, false, + localState, messages, events); + } + + private StateUpdate noUpdate( + BdfDictionary localState, boolean delete) throws FormatException { + + return new StateUpdate(delete, false, + localState, Collections.emptyList(), + Collections.emptyList()); + } +} diff --git a/briar-core/src/org/briarproject/forum/SharerEngine.java b/briar-core/src/org/briarproject/forum/SharerEngine.java new file mode 100644 index 000000000..05db965b8 --- /dev/null +++ b/briar-core/src/org/briarproject/forum/SharerEngine.java @@ -0,0 +1,264 @@ +package org.briarproject.forum; + +import org.briarproject.api.FormatException; +import org.briarproject.api.clients.ProtocolEngine; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.ForumInvitationResponseReceivedEvent; +import org.briarproject.api.forum.SharerAction; +import org.briarproject.api.forum.SharerProtocolState; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.forum.ForumConstants.CONTACT_ID; +import static org.briarproject.api.forum.ForumConstants.FORUM_NAME; +import static org.briarproject.api.forum.ForumConstants.FORUM_SALT; +import static org.briarproject.api.forum.ForumConstants.GROUP_ID; +import static org.briarproject.api.forum.ForumConstants.INVITATION_MSG; +import static org.briarproject.api.forum.ForumConstants.SESSION_ID; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION; +import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE; +import static org.briarproject.api.forum.ForumConstants.STATE; +import static org.briarproject.api.forum.ForumConstants.TASK; +import static org.briarproject.api.forum.ForumConstants.TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US; +import static org.briarproject.api.forum.ForumConstants.TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US; +import static org.briarproject.api.forum.ForumConstants.TASK_SHARE_FORUM; +import static org.briarproject.api.forum.ForumConstants.TASK_UNSHARE_FORUM_SHARED_BY_US; +import static org.briarproject.api.forum.ForumConstants.TYPE; +import static org.briarproject.api.forum.SharerAction.LOCAL_ABORT; +import static org.briarproject.api.forum.SharerAction.LOCAL_INVITATION; +import static org.briarproject.api.forum.SharerAction.LOCAL_LEAVE; +import static org.briarproject.api.forum.SharerAction.REMOTE_ACCEPT; +import static org.briarproject.api.forum.SharerAction.REMOTE_DECLINE; +import static org.briarproject.api.forum.SharerAction.REMOTE_LEAVE; +import static org.briarproject.api.forum.SharerProtocolState.ERROR; +import static org.briarproject.api.forum.SharerProtocolState.FINISHED; +import static org.briarproject.api.forum.SharerProtocolState.LEFT; + +public class SharerEngine + implements ProtocolEngine { + + private static final Logger LOG = + Logger.getLogger(SharerEngine.class.getName()); + + @Override + public StateUpdate onLocalAction( + BdfDictionary localState, BdfDictionary localAction) { + + try { + SharerProtocolState currentState = + getState(localState.getLong(STATE)); + long type = localAction.getLong(TYPE); + SharerAction action = SharerAction.getLocal(type); + SharerProtocolState nextState = currentState.next(action); + localState.put(STATE, nextState.getValue()); + + if (action == LOCAL_ABORT && currentState != ERROR) { + return abortSession(currentState, localState); + } + + if (nextState == ERROR) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("Error: Invalid action in state " + + currentState.name()); + } + return noUpdate(localState, true); + } + List messages; + List events = Collections.emptyList(); + + if (action == LOCAL_INVITATION) { + BdfDictionary msg = new BdfDictionary(); + msg.put(TYPE, SHARE_MSG_TYPE_INVITATION); + msg.put(SESSION_ID, localState.getRaw(SESSION_ID)); + msg.put(GROUP_ID, localState.getRaw(GROUP_ID)); + msg.put(FORUM_NAME, localState.getString(FORUM_NAME)); + msg.put(FORUM_SALT, localState.getRaw(FORUM_SALT)); + if (localAction.containsKey(INVITATION_MSG)) { + msg.put(INVITATION_MSG, + localAction.getString(INVITATION_MSG)); + } + messages = Collections.singletonList(msg); + logLocalAction(currentState, localState, msg); + + // remember that we offered to share this forum + localState.put(TASK, TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US); + } + else if (action == LOCAL_LEAVE) { + BdfDictionary msg = new BdfDictionary(); + msg.put(TYPE, SHARE_MSG_TYPE_LEAVE); + msg.put(SESSION_ID, localState.getRaw(SESSION_ID)); + msg.put(GROUP_ID, localState.getRaw(GROUP_ID)); + messages = Collections.singletonList(msg); + logLocalAction(currentState, localState, msg); + } + else { + throw new IllegalArgumentException("Unknown Local Action"); + } + return new StateUpdate(false, + false, localState, messages, events); + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public StateUpdate onMessageReceived( + BdfDictionary localState, BdfDictionary msg) { + + try { + SharerProtocolState currentState = + getState(localState.getLong(STATE)); + long type = msg.getLong(TYPE); + SharerAction action = SharerAction.getRemote(type); + SharerProtocolState nextState = currentState.next(action); + localState.put(STATE, nextState.getValue()); + + logMessageReceived(currentState, nextState, type, msg); + + if (nextState == ERROR) { + if (currentState != ERROR) { + return abortSession(currentState, localState); + } else { + return noUpdate(localState, true); + } + } + List messages = Collections.emptyList(); + List events = Collections.emptyList(); + boolean deleteMsg = false; + + if (currentState == LEFT) { + // ignore and delete messages coming in while in that state + deleteMsg = true; + } + else if (action == REMOTE_LEAVE) { + localState.put(TASK, TASK_UNSHARE_FORUM_SHARED_BY_US); + } + else if (currentState == FINISHED) { + // ignore and delete messages coming in while in that state + // note that LEAVE is possible, but was handled above + deleteMsg = true; + } + // we have sent our invitation and just got a response + else if (action == REMOTE_ACCEPT || action == REMOTE_DECLINE) { + if (action == REMOTE_ACCEPT) { + localState.put(TASK, TASK_SHARE_FORUM); + } else { + // this ensures that the forum can be shared again + localState.put(TASK, + TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US); + } + String name = localState.getString(FORUM_NAME); + ContactId c = new ContactId( + localState.getLong(CONTACT_ID).intValue()); + Event event = new ForumInvitationResponseReceivedEvent(name, c); + events = Collections.singletonList(event); + } + else { + throw new IllegalArgumentException("Bad state"); + } + return new StateUpdate(deleteMsg, + false, localState, messages, events); + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + private void logLocalAction(SharerProtocolState state, + BdfDictionary localState, BdfDictionary msg) { + + if (!LOG.isLoggable(INFO)) return; + + String a = "invitation"; + if (msg.getLong(TYPE, -1L) == SHARE_MSG_TYPE_LEAVE) a = "leave"; + + try { + LOG.info("Sending " + a + " in state " + state.name() + + " with session ID " + + Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " + + Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " + + "Moving on to state " + + getState(localState.getLong(STATE)).name() + ); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + + private void logMessageReceived(SharerProtocolState currentState, + SharerProtocolState nextState, long type, BdfDictionary msg) { + if (!LOG.isLoggable(INFO)) return; + + try { + String t = "unknown"; + if (type == SHARE_MSG_TYPE_ACCEPT) t = "ACCEPT"; + else if (type == SHARE_MSG_TYPE_DECLINE) t = "DECLINE"; + else if (type == SHARE_MSG_TYPE_LEAVE) t = "LEAVE"; + else if (type == SHARE_MSG_TYPE_ABORT) t = "ABORT"; + + LOG.info("Received " + t + " in state " + currentState.name() + + " with session ID " + + Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " + + Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " + + "Moving on to state " + nextState.name() + ); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + + @Override + public StateUpdate onMessageDelivered( + BdfDictionary localState, BdfDictionary delivered) { + try { + return noUpdate(localState, false); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + return null; + } + } + + private SharerProtocolState getState(Long state) { + return SharerProtocolState.fromValue(state.intValue()); + } + + private StateUpdate abortSession( + SharerProtocolState currentState, BdfDictionary localState) + throws FormatException { + + if (LOG.isLoggable(WARNING)) { + LOG.warning("Aborting protocol session " + + Arrays.hashCode(localState.getRaw(SESSION_ID)) + + " in state " + currentState.name()); + } + + localState.put(STATE, ERROR.getValue()); + BdfDictionary msg = new BdfDictionary(); + msg.put(TYPE, SHARE_MSG_TYPE_ABORT); + msg.put(SESSION_ID, localState.getRaw(SESSION_ID)); + msg.put(GROUP_ID, localState.getRaw(GROUP_ID)); + List messages = Collections.singletonList(msg); + + List events = Collections.emptyList(); + + return new StateUpdate(false, false, + localState, messages, events); + } + + private StateUpdate noUpdate( + BdfDictionary localState, boolean delete) throws FormatException { + + return new StateUpdate(delete, false, + localState, Collections.emptyList(), + Collections.emptyList()); + } +} diff --git a/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java b/briar-tests/src/org/briarproject/forum/ForumSharingValidatorTest.java similarity index 77% rename from briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java rename to briar-tests/src/org/briarproject/forum/ForumSharingValidatorTest.java index e10623d58..0b87d04c1 100644 --- a/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java +++ b/briar-tests/src/org/briarproject/forum/ForumSharingValidatorTest.java @@ -5,7 +5,7 @@ import org.junit.Test; import static org.junit.Assert.fail; -public class ForumListValidatorTest extends BriarTestCase { +public class ForumSharingValidatorTest extends BriarTestCase { @Test public void testUnitTestsExist() {