From 9bef114c350cfb2256ca42b372b57c0296d86b25 Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
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        | 821 ++++++++++++++----
 .../forum/ForumSharingValidator.java          |  81 ++
 .../org/briarproject/forum/InviteeEngine.java | 265 ++++++
 .../org/briarproject/forum/SharerEngine.java  | 264 ++++++
 ...st.java => ForumSharingValidatorTest.java} |   2 +-
 23 files changed, 1688 insertions(+), 313 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 e955ea3a47..565f9f1f8d 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 76eb803a35..2fc96b6ab7 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 eb4ade9abe..f685e0d067 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 cccecbc7ae..ccb0b365d9 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<ContactId> 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 0000000000..1d8e7b5f7b
--- /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 0000000000..1e79924038
--- /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 035a06cd1e..dcdee0132b 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 0000000000..b153c27f11
--- /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 e3e3b1191c..9f4cc87b66 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<ForumInvitationMessage> getForumInvitationMessages(
+			ContactId contactId) throws DbException;
+
 	/** Returns all forums to which the user could subscribe. */
 	Collection<Forum> 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<Contact> getSharedBy(GroupId g) throws DbException;
 
 	/** Returns the IDs of all contacts with whom the given forum is shared. */
 	Collection<ContactId> getSharedWith(GroupId g) throws DbException;
 
-	/**
-	 * Shares a forum with the given contacts and unshares it with any other
-	 * contacts.
-	 */
-	void setSharedWith(GroupId g, Collection<ContactId> 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 0000000000..212f0861ce
--- /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 0000000000..35d6a880af
--- /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 0000000000..39796f2c82
--- /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 0000000000..b948a9483d
--- /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 8c9de76dfa..d860726a75 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 0000000000..c83350a09e
--- /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 9db8854a1d..f1c8eb51fe 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 9e6947a377..0000000000
--- 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 ac0ff20616..e6d7b55ac4 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 62501e8e50..c970571a9c 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,30 +163,228 @@ 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<MessageId, BdfDictionary> map = clientHelper
+					.getMessageMetadataAsDictionary(txn, localGroup.getId());
+			for (Map.Entry<MessageId, BdfDictionary> 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)
+	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");
+		}
+	}
+
+	@Override
+	public ClientId getClientId() {
+		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 {
-			ContactId contactId = getContactId(txn, m.getGroupId());
-			setForumVisibility(txn, contactId, getVisibleForums(txn, m));
+			// 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 ClientId getClientId() {
-		return CLIENT_ID;
+	public Collection<ForumInvitationMessage> getForumInvitationMessages(
+			ContactId contactId) throws DbException {
+
+		Transaction txn = db.startTransaction(false);
+		try {
+			Contact contact = db.getContact(txn, contactId);
+			Group group = getContactGroup(contact);
+
+			Collection<ForumInvitationMessage> list =
+					new ArrayList<ForumInvitationMessage>();
+			Map<MessageId, BdfDictionary> map = clientHelper
+					.getMessageMetadataAsDictionary(txn, group.getId());
+			for (Map.Entry<MessageId, BdfDictionary> 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<Forum> 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<ContactId> shared)
+	public boolean canBeShared(GroupId g, Contact c) throws DbException {
+		boolean canBeShared;
+		Transaction txn = db.startTransaction(true);
+		try {
+			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 {
-			Transaction txn = db.startTransaction(false);
+			// 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<MessageId, BdfDictionary> map = clientHelper
+					.getMessageMetadataAsDictionary(txn, localGroup.getId());
+			for (Map.Entry<MessageId, BdfDictionary> 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<MessageId, BdfDictionary> map = clientHelper
+				.getMessageMetadataAsDictionary(txn, localGroup.getId());
+		for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
+			BdfDictionary d = m.getValue();
 			try {
-				// Retrieve the forum
-				Forum f = parseForum(db.getGroup(txn, g));
-				// Update the list shared with each contact
-				shared = new HashSet<ContactId>(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);
+				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;
 					}
 				}
-				txn.setComplete();
-			} finally {
-				db.endTransaction(txn);
+			} catch (FormatException e) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			}
-		} catch (FormatException e) {
-			throw new DbException(e);
 		}
+		throw new DbException();
 	}
 
-	private Group getContactGroup(Contact c) {
-		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
+	private BdfDictionary getSessionStateForLeaving(Transaction txn, Forum f,
+			ContactId c) throws DbException, FormatException {
+
+		Map<MessageId, BdfDictionary> map = clientHelper
+				.getMessageMetadataAsDictionary(txn, localGroup.getId());
+		for (Map.Entry<MessageId, BdfDictionary> 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 LatestUpdate findLatest(Transaction txn, GroupId g, boolean local)
+	private void processStateUpdate(Transaction txn, MessageId messageId,
+			StateUpdate<BdfDictionary, BdfDictionary> result)
 			throws DbException, FormatException {
-		LatestUpdate latest = null;
-		Map<MessageId, BdfDictionary> metadata =
-				clientHelper.getMessageMetadataAsDictionary(txn, g);
-		for (Entry<MessageId, BdfDictionary> 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);
+
+		// 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);
 		}
-		return latest;
 	}
 
-	private List<Forum> parseForumList(BdfList message) throws FormatException {
-		// Version, forum list
-		BdfList forumList = message.getList(1);
-		List<Forum> forums = new ArrayList<Forum>(forumList.size());
-		for (int i = 0; i < forumList.size(); i++) {
-			// Name, salt
-			BdfList forum = forumList.getList(i);
-			forums.add(forumManager
-					.createForum(forum.getString(0), forum.getRaw(1)));
+	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);
 		}
-		return forums;
 	}
 
-	private void storeMessage(Transaction txn, GroupId g, List<Forum> 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 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 BdfList encodeForumList(List<Forum> 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 Group getContactGroup(Contact c) {
+		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
 	}
 
 	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<GroupId> 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<Forum> shared = new HashSet<Forum>(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<GroupId> visible = new HashSet<GroupId>(shared.size());
-		for (Forum f : shared) visible.add(f.getId());
-		return visible;
-	}
-
-	private void setForumVisibility(Transaction txn, ContactId c,
-			Set<GroupId> 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);
-		}
+		return new ContactId(meta.getLong(CONTACT_ID).intValue());
 	}
 
-	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 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 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<Forum> list = parseForumList(message);
-		for (Forum f : list) if (f.getId().equals(forum)) return true;
+	private boolean listContains(Transaction txn, GroupId contactGroup,
+			GroupId forum, String key) throws DbException, FormatException {
+
+		List<Forum> list = getForumList(txn, contactGroup, key);
+		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);
+	private boolean addToList(Transaction txn, GroupId groupId, String key,
+			Forum f) throws DbException, FormatException {
+
+		List<Forum> 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<Forum> forums = getForumList(txn, groupId, key);
+		if (forums.remove(f)) {
+			storeForumList(txn, groupId, key, forums);
 			return true;
 		}
-		BdfList message = clientHelper.getMessageAsList(txn, latest.messageId);
-		List<Forum> list = parseForumList(message);
-		if (list.contains(f)) return false;
-		list.add(f);
-		storeMessage(txn, g, list, latest.version + 1);
-		return true;
+		return false;
 	}
 
-	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<Forum> list = parseForumList(message);
-		if (list.remove(f)) storeMessage(txn, g, list, latest.version + 1);
+	private List<Forum> 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 static class LatestUpdate {
+	private void storeForumList(Transaction txn, GroupId groupId, String key,
+			List<Forum> forums)	throws DbException, FormatException {
+
+		BdfList list = encodeForumList(forums);
+		BdfDictionary metadata = BdfDictionary.of(
+				new BdfEntry(key, list)
+		);
+		clientHelper.mergeGroupMetadata(txn, groupId, metadata);
+	}
 
-		private final MessageId messageId;
-		private final long version;
+	private BdfList encodeForumList(List<Forum> forums) {
+		BdfList forumList = new BdfList();
+		for (Forum f : forums)
+			forumList.add(BdfList.of(f.getName(), f.getSalt()));
+		return forumList;
+	}
 
-		private LatestUpdate(MessageId messageId, long version) {
-			this.messageId = messageId;
-			this.version = version;
+	private List<Forum> parseForumList(BdfList list) throws FormatException {
+		List<Forum> forums = new ArrayList<Forum>(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 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/org/briarproject/forum/ForumSharingValidator.java b/briar-core/src/org/briarproject/forum/ForumSharingValidator.java
new file mode 100644
index 0000000000..c204623a27
--- /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 0000000000..f208204db6
--- /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<BdfDictionary, BdfDictionary, BdfDictionary> {
+
+	private static final Logger LOG =
+			Logger.getLogger(SharerEngine.class.getName());
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages;
+			List<Event> 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<BdfDictionary, BdfDictionary>(false,
+					false, localState, messages, events);
+		} catch (FormatException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages = Collections.emptyList();
+			List<Event> 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<BdfDictionary, BdfDictionary>(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<BdfDictionary, BdfDictionary> 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<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages = Collections.singletonList(msg);
+
+		List<Event> events = Collections.emptyList();
+
+		return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
+				localState, messages, events);
+	}
+
+	private StateUpdate<BdfDictionary, BdfDictionary> noUpdate(
+			BdfDictionary localState, boolean delete) throws FormatException {
+
+		return new StateUpdate<BdfDictionary, BdfDictionary>(delete, false,
+				localState, Collections.<BdfDictionary>emptyList(),
+				Collections.<Event>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 0000000000..05db965b80
--- /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<BdfDictionary, BdfDictionary, BdfDictionary> {
+
+	private static final Logger LOG =
+			Logger.getLogger(SharerEngine.class.getName());
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages;
+			List<Event> 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<BdfDictionary, BdfDictionary>(false,
+					false, localState, messages, events);
+		} catch (FormatException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages = Collections.emptyList();
+			List<Event> 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<BdfDictionary, BdfDictionary>(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<BdfDictionary, BdfDictionary> 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<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages = Collections.singletonList(msg);
+
+		List<Event> events = Collections.emptyList();
+
+		return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
+				localState, messages, events);
+	}
+
+	private StateUpdate<BdfDictionary, BdfDictionary> noUpdate(
+			BdfDictionary localState, boolean delete) throws FormatException {
+
+		return new StateUpdate<BdfDictionary, BdfDictionary>(delete, false,
+				localState, Collections.<BdfDictionary>emptyList(),
+				Collections.<Event>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 e10623d584..0b87d04c19 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() {
-- 
GitLab