Merge branch '322-forum-sharing-client' into 'master'

Forum Sharing Client Backend

This MR 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 invitation. Also, the
Forum Sharing Manager is notified when users leave a forum.

Please note that you will need the UI to actually test this. It is coming up soon in another MR.

Closes #322

See merge request !170
This commit is contained in:
akwizgran
2016-05-09 16:30:21 +00:00
28 changed files with 1735 additions and 353 deletions

View File

@@ -8,7 +8,7 @@ abstract class ConversationIntroductionItem extends ConversationItem {
private boolean answered; private boolean answered;
public ConversationIntroductionItem(IntroductionRequest ir) { public ConversationIntroductionItem(IntroductionRequest ir) {
super(ir.getMessageId(), ir.getTime()); super(ir.getMessageId(), ir.getTimestamp());
this.ir = ir; this.ir = ir;
this.answered = ir.wasAnswered(); this.answered = ir.wasAnswered();

View File

@@ -69,7 +69,7 @@ public abstract class ConversationItem {
ir.getName()); ir.getName());
} }
return new ConversationNoticeOutItem(ir.getMessageId(), text, return new ConversationNoticeOutItem(ir.getMessageId(), text,
ir.getTime(), ir.isSent(), ir.isSeen()); ir.getTimestamp(), ir.isSent(), ir.isSeen());
} else { } else {
String text; String text;
if (ir.wasAccepted()) { if (ir.wasAccepted()) {
@@ -88,7 +88,7 @@ public abstract class ConversationItem {
} }
} }
return new ConversationNoticeInItem(ir.getMessageId(), text, 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) { public static ConversationItem from(IntroductionMessage im) {
if (im.isLocal()) if (im.isLocal())
return new ConversationNoticeOutItem(im.getMessageId(), "", return new ConversationNoticeOutItem(im.getMessageId(), "",
im.getTime(), false, false); im.getTimestamp(), false, false);
return new ConversationNoticeInItem(im.getMessageId(), "", im.getTime(), return new ConversationNoticeInItem(im.getMessageId(), "",
im.isRead()); im.getTimestamp(), im.isRead());
} }
protected interface OutgoingItem { protected interface OutgoingItem {

View File

@@ -171,7 +171,6 @@ implements EventListener, OnItemClickListener {
public void run() { public void run() {
try { try {
forumManager.addForum(f); forumManager.addForum(f);
forumSharingManager.setSharedWith(f.getId(), shared);
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);

View File

@@ -91,7 +91,6 @@ public class ShareForumActivity extends BriarActivity implements
onBackPressed(); onBackPressed();
return true; return true;
case R.id.action_share_forum: case R.id.action_share_forum:
storeVisibility();
return true; return true;
default: default:
return super.onOptionsItemSelected(item); 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 @Override
public void onItemClick(View view, ContactListItem item) { public void onItemClick(View view, ContactListItem item) {
((SelectableContactListItem) item).toggleSelected(); ((SelectableContactListItem) item).toggleSelected();

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -15,4 +15,38 @@ public interface ForumConstants {
/** The maximum length of a forum post's body in bytes. */ /** The maximum length of a forum post's body in bytes. */
int MAX_FORUM_POST_BODY_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024; 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;
} }

View File

@@ -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;
}
}

View File

@@ -31,6 +31,9 @@ public interface ForumManager {
/** Returns the forum with the given ID. */ /** Returns the forum with the given ID. */
Forum getForum(GroupId g) throws DbException; Forum getForum(GroupId g) throws DbException;
/** Returns the forum with the given ID. */
Forum getForum(Transaction txn, GroupId g) throws DbException;
/** Returns all forums to which the user subscribes. */ /** Returns all forums to which the user subscribes. */
Collection<Forum> getForums() throws DbException; Collection<Forum> getForums() throws DbException;

View File

@@ -1,5 +1,6 @@
package org.briarproject.api.forum; package org.briarproject.api.forum;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
@@ -13,20 +14,35 @@ public interface ForumSharingManager {
/** Returns the unique ID of the forum sharing client. */ /** Returns the unique ID of the forum sharing client. */
ClientId getClientId(); 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. */ /** Returns all forums to which the user could subscribe. */
Collection<Forum> getAvailableForums() throws DbException; 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; Collection<Contact> getSharedBy(GroupId g) throws DbException;
/** Returns the IDs of all contacts with whom the given forum is shared. */ /** Returns the IDs of all contacts with whom the given forum is shared. */
Collection<ContactId> getSharedWith(GroupId g) throws DbException; Collection<ContactId> getSharedWith(GroupId g) throws DbException;
/** /** Returns true if the forum not already shared and no invitation is open */
* Shares a forum with the given contacts and unshares it with any other boolean canBeShared(GroupId g, Contact c) throws DbException;
* contacts.
*/
void setSharedWith(GroupId g, Collection<ContactId> shared)
throws DbException;
} }

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,61 +1,36 @@
package org.briarproject.api.introduction; package org.briarproject.api.introduction;
import org.briarproject.api.clients.SessionId; import org.briarproject.api.clients.SessionId;
import org.briarproject.api.messaging.BaseMessage;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE; import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER; import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
abstract public class IntroductionMessage { public class IntroductionMessage extends BaseMessage {
private final SessionId sessionId; private final SessionId sessionId;
private final MessageId messageId; private final MessageId messageId;
private final int role; private final int role;
private final long time;
private final boolean local, sent, seen, read;
public IntroductionMessage(SessionId sessionId, MessageId messageId, public IntroductionMessage(SessionId sessionId, MessageId messageId,
int role, long time, boolean local, boolean sent, boolean seen, int role, long time, boolean local, boolean sent, boolean seen,
boolean read) { boolean read) {
super(messageId, time, local, read, sent, seen);
this.sessionId = sessionId; this.sessionId = sessionId;
this.messageId = messageId; this.messageId = messageId;
this.role = role; this.role = role;
this.time = time;
this.local = local;
this.sent = sent;
this.seen = seen;
this.read = read;
} }
public SessionId getSessionId() { public SessionId getSessionId() {
return sessionId; return sessionId;
} }
public long getTime() {
return time;
}
public MessageId getMessageId() { public MessageId getMessageId() {
return messageId; 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() { public boolean isIntroducer() {
return role == ROLE_INTRODUCER; return role == ROLE_INTRODUCER;
} }

View File

@@ -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;
}
}

View File

@@ -2,50 +2,19 @@ package org.briarproject.api.messaging;
import org.briarproject.api.sync.MessageId; 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 String contentType;
private final boolean local, read, sent, seen;
public PrivateMessageHeader(MessageId id, long timestamp, public PrivateMessageHeader(MessageId id, long timestamp,
String contentType, boolean local, boolean read, boolean sent, String contentType, boolean local, boolean read, boolean sent,
boolean seen) { 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() { super(id, timestamp, local, read, sent, seen);
return id; this.contentType = contentType;
} }
public String getContentType() { public String getContentType() {
return contentType; 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;
}
} }

View File

@@ -24,7 +24,7 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook,
protected final MetadataParser metadataParser; protected final MetadataParser metadataParser;
protected BdfIncomingMessageHook(ClientHelper clientHelper, protected BdfIncomingMessageHook(ClientHelper clientHelper,
MetadataParser metadataParser, Clock clock) { MetadataParser metadataParser) {
this.clientHelper = clientHelper; this.clientHelper = clientHelper;
this.metadataParser = metadataParser; this.metadataParser = metadataParser;
} }

View File

@@ -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;
}
}

View File

@@ -141,15 +141,21 @@ class ForumManagerImpl implements ForumManager {
@Override @Override
public Forum getForum(GroupId g) throws DbException { public Forum getForum(GroupId g) throws DbException {
try { Forum forum;
Group group;
Transaction txn = db.startTransaction(true); Transaction txn = db.startTransaction(true);
try { try {
group = db.getGroup(txn, g); forum = getForum(txn, g);
txn.setComplete(); txn.setComplete();
} finally { } finally {
db.endTransaction(txn); db.endTransaction(txn);
} }
return forum;
}
@Override
public Forum getForum(Transaction txn, GroupId g) throws DbException {
try {
Group group = db.getGroup(txn, g);
return parseForum(group); return parseForum(group);
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);

View File

@@ -1,6 +1,7 @@
package org.briarproject.forum; package org.briarproject.forum;
import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.clients.MessageQueueManager;
import org.briarproject.api.contact.ContactManager; import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.data.MetadataEncoder; import org.briarproject.api.data.MetadataEncoder;
@@ -26,11 +27,11 @@ import dagger.Provides;
public class ForumModule { public class ForumModule {
public static class EagerSingletons { public static class EagerSingletons {
@Inject
ForumListValidator forumListValidator;
@Inject @Inject
ForumPostValidator forumPostValidator; ForumPostValidator forumPostValidator;
@Inject @Inject
ForumSharingValidator forumSharingValidator;
@Inject
ForumSharingManager forumSharingManager; ForumSharingManager forumSharingManager;
} }
@@ -63,13 +64,15 @@ public class ForumModule {
@Provides @Provides
@Singleton @Singleton
ForumListValidator provideForumListValidator( ForumSharingValidator provideSharingValidator(
ValidationManager validationManager, ClientHelper clientHelper, MessageQueueManager messageQueueManager, ClientHelper clientHelper,
MetadataEncoder metadataEncoder, Clock clock) { MetadataEncoder metadataEncoder, Clock clock) {
ForumListValidator validator = new ForumListValidator(clientHelper,
ForumSharingValidator validator = new ForumSharingValidator(clientHelper,
metadataEncoder, clock); metadataEncoder, clock);
validationManager.registerMessageValidator( messageQueueManager.registerMessageValidator(
ForumSharingManagerImpl.CLIENT_ID, validator); ForumSharingManagerImpl.CLIENT_ID, validator);
return validator; return validator;
} }
@@ -78,15 +81,17 @@ public class ForumModule {
ForumSharingManager provideForumSharingManager( ForumSharingManager provideForumSharingManager(
LifecycleManager lifecycleManager, LifecycleManager lifecycleManager,
ContactManager contactManager, ContactManager contactManager,
ValidationManager validationManager, MessageQueueManager messageQueueManager,
ForumManager forumManager, ForumManager forumManager,
ForumSharingManagerImpl forumSharingManager) { ForumSharingManagerImpl forumSharingManager) {
lifecycleManager.registerClient(forumSharingManager); lifecycleManager.registerClient(forumSharingManager);
contactManager.registerAddContactHook(forumSharingManager); contactManager.registerAddContactHook(forumSharingManager);
contactManager.registerRemoveContactHook(forumSharingManager); contactManager.registerRemoveContactHook(forumSharingManager);
validationManager.registerIncomingMessageHook( messageQueueManager.registerIncomingMessageHook(
ForumSharingManagerImpl.CLIENT_ID, forumSharingManager); ForumSharingManagerImpl.CLIENT_ID, forumSharingManager);
forumManager.registerRemoveForumHook(forumSharingManager); forumManager.registerRemoveForumHook(forumSharingManager);
return forumSharingManager; return forumSharingManager;
} }
} }

View File

@@ -1,74 +1,140 @@
package org.briarproject.forum; package org.briarproject.forum;
import org.briarproject.api.Bytes;
import org.briarproject.api.FormatException; import org.briarproject.api.FormatException;
import org.briarproject.api.clients.Client; import org.briarproject.api.clients.Client;
import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.clients.MessageQueueManager;
import org.briarproject.api.clients.PrivateGroupFactory; import org.briarproject.api.clients.PrivateGroupFactory;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager.AddContactHook; import org.briarproject.api.contact.ContactManager.AddContactHook;
import org.briarproject.api.contact.ContactManager.RemoveContactHook; import org.briarproject.api.contact.ContactManager.RemoveContactHook;
import org.briarproject.api.data.BdfDictionary; import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfEntry;
import org.briarproject.api.data.BdfList; 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.DatabaseComponent;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Metadata; import org.briarproject.api.db.Metadata;
import org.briarproject.api.db.NoSuchMessageException;
import org.briarproject.api.db.Transaction; import org.briarproject.api.db.Transaction;
import org.briarproject.api.event.Event;
import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumInvitationMessage;
import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumManager;
import org.briarproject.api.forum.ForumSharingManager; 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.ClientId;
import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId; 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.api.system.Clock;
import org.briarproject.clients.BdfIncomingMessageHook;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.logging.Logger;
import javax.inject.Inject; 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.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, class ForumSharingManagerImpl extends BdfIncomingMessageHook
AddContactHook, RemoveContactHook, IncomingMessageHook, implements ForumSharingManager, Client, RemoveForumHook,
RemoveForumHook { AddContactHook, RemoveContactHook {
static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString( static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString(
"cd11a5d04dccd9e2931d6fc3df456313" "cd11a5d04dccd9e2931d6fc3df456313"
+ "63bb3e9d9d0e9405fccdb051f41f5449")); + "63bb3e9d9d0e9405fccdb051f41f5449"));
private static final Logger LOG =
Logger.getLogger(ForumSharingManagerImpl.class.getName());
private final DatabaseComponent db; private final DatabaseComponent db;
private final ForumManager forumManager; 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 PrivateGroupFactory privateGroupFactory;
private final Clock clock; private final Clock clock;
private final Group localGroup;
@Inject @Inject
ForumSharingManagerImpl(DatabaseComponent db, ForumManager forumManager, ForumSharingManagerImpl(DatabaseComponent db, ForumManager forumManager,
ClientHelper clientHelper, PrivateGroupFactory privateGroupFactory, MessageQueueManager messageQueueManager, ClientHelper clientHelper,
MetadataParser metadataParser, MetadataEncoder metadataEncoder,
SecureRandom random, PrivateGroupFactory privateGroupFactory,
Clock clock) { Clock clock) {
super(clientHelper, metadataParser);
this.db = db; this.db = db;
this.forumManager = forumManager; this.forumManager = forumManager;
this.clientHelper = clientHelper; this.messageQueueManager = messageQueueManager;
this.metadataEncoder = metadataEncoder;
this.random = random;
this.privateGroupFactory = privateGroupFactory; this.privateGroupFactory = privateGroupFactory;
this.clock = clock; this.clock = clock;
localGroup = privateGroupFactory.createLocalGroup(getClientId());
} }
@Override @Override
public void createLocalState(Transaction txn) throws DbException { public void createLocalState(Transaction txn) throws DbException {
db.addGroup(txn, localGroup);
// Ensure we've set things up for any pre-existing contacts // Ensure we've set things up for any pre-existing contacts
for (Contact c : db.getContacts(txn)) addingContact(txn, c); 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); db.setVisibleToContact(txn, c.getId(), g.getId(), true);
// Attach the contact ID to the group // Attach the contact ID to the group
BdfDictionary meta = new BdfDictionary(); 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); clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);
@@ -94,17 +163,88 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
@Override @Override
public void removingContact(Transaction txn, Contact c) throws DbException { 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)); db.removeGroup(txn, getContactGroup(c));
} }
@Override @Override
public void incomingMessage(Transaction txn, Message m, Metadata meta) protected void incomingMessage(Transaction txn, Message m, BdfList body,
throws DbException { 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 { try {
ContactId contactId = getContactId(txn, m.getGroupId()); // check if we have a session with that ID already
setForumVisibility(txn, contactId, getVisibleForums(txn, m)); getSessionState(txn, sessionId, false);
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); // this is what we would expect under normal circumstances
stateExists = false;
}
try {
// check if we already have a state with that sessionId
if (stateExists) throw new FormatException();
// check if forum can be shared
Forum f = forumManager.createForum(msg.getString(FORUM_NAME),
msg.getRaw(FORUM_SALT));
ContactId contactId = getContactId(txn, m.getGroupId());
Contact contact = db.getContact(txn, contactId);
if (!canBeShared(txn, f.getId(), contact))
throw new FormatException();
// initialize state and process invitation
BdfDictionary state =
initializeInviteeState(txn, contactId, msg);
InviteeEngine engine = new InviteeEngine();
processStateUpdate(txn, m.getId(),
engine.onMessageReceived(state, msg));
} catch (FormatException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
deleteMessage(txn, m.getId());
}
} else if (type == SHARE_MSG_TYPE_ACCEPT ||
type == SHARE_MSG_TYPE_DECLINE) {
// we are a sharer who just received a response
BdfDictionary state = getSessionState(txn, sessionId, true);
SharerEngine engine = new SharerEngine();
processStateUpdate(txn, m.getId(),
engine.onMessageReceived(state, msg));
} else if (type == SHARE_MSG_TYPE_LEAVE ||
type == SHARE_MSG_TYPE_ABORT) {
// we don't know who we are, so figure it out
BdfDictionary state = getSessionState(txn, sessionId, true);
if (state.getBoolean(IS_SHARER)) {
// we are a sharer and the invitee wants to leave or abort
SharerEngine engine = new SharerEngine();
processStateUpdate(txn, m.getId(),
engine.onMessageReceived(state, msg));
} else {
// we are an invitee and the sharer wants to leave or abort
InviteeEngine engine = new InviteeEngine();
processStateUpdate(txn, m.getId(),
engine.onMessageReceived(state, msg));
}
} else {
// message has passed validator, so that should never happen
throw new RuntimeException("Illegal Forum Sharing Message");
} }
} }
@@ -113,11 +253,138 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
return CLIENT_ID; return CLIENT_ID;
} }
@Override
public void sendForumInvitation(GroupId groupId, ContactId contactId,
String msg) throws DbException {
Transaction txn = db.startTransaction(false);
try {
// initialize local state for sharer
Forum f = forumManager.getForum(txn, groupId);
BdfDictionary localState = initializeSharerState(txn, f, contactId);
// define action
BdfDictionary localAction = new BdfDictionary();
localAction.put(TYPE, SHARE_MSG_TYPE_INVITATION);
if (!StringUtils.isNullOrEmpty(msg)) {
localAction.put(INVITATION_MSG, msg);
}
// start engine and process its state update
SharerEngine engine = new SharerEngine();
processStateUpdate(txn, null,
engine.onLocalAction(localState, localAction));
txn.setComplete();
} catch (FormatException e) {
throw new DbException();
} finally {
db.endTransaction(txn);
}
}
@Override
public void respondToInvitation(Forum f, boolean accept)
throws DbException {
Transaction txn = db.startTransaction(false);
try {
// find session state based on forum
BdfDictionary localState = getSessionStateForResponse(txn, f);
// define action
BdfDictionary localAction = new BdfDictionary();
if (accept) {
localAction.put(TYPE, SHARE_MSG_TYPE_ACCEPT);
} else {
localAction.put(TYPE, SHARE_MSG_TYPE_DECLINE);
}
// start engine and process its state update
InviteeEngine engine = new InviteeEngine();
processStateUpdate(txn, null,
engine.onLocalAction(localState, localAction));
txn.setComplete();
} catch (FormatException e) {
throw new DbException(e);
} finally {
db.endTransaction(txn);
}
}
@Override
public Collection<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 @Override
public void removingForum(Transaction txn, Forum f) throws DbException { public void removingForum(Transaction txn, Forum f) throws DbException {
try { try {
for (Contact c : db.getContacts(txn)) for (Contact c : db.getContacts(txn)) {
removeFromList(txn, getContactGroup(c).getId(), f); 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) { } catch (IOException e) {
throw new DbException(e); throw new DbException(e);
} }
@@ -135,18 +402,13 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
// Get all forums shared by contacts // Get all forums shared by contacts
for (Contact c : db.getContacts(txn)) { for (Contact c : db.getContacts(txn)) {
Group g = getContactGroup(c); Group g = getContactGroup(c);
// Find the latest update version List<Forum> forums =
LatestUpdate latest = findLatest(txn, g.getId(), false); getForumList(txn, g.getId(), SHARED_WITH_US);
if (latest != null) { for (Forum f : forums) {
// Retrieve and parse the latest update
BdfList message = clientHelper.getMessageAsList(txn,
latest.messageId);
for (Forum f : parseForumList(message)) {
if (!subscribed.contains(f.getGroup())) if (!subscribed.contains(f.getGroup()))
available.add(f); available.add(f);
} }
} }
}
txn.setComplete(); txn.setComplete();
} finally { } finally {
db.endTransaction(txn); db.endTransaction(txn);
@@ -164,7 +426,8 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
Transaction txn = db.startTransaction(true); Transaction txn = db.startTransaction(true);
try { try {
for (Contact c : db.getContacts(txn)) { 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); subscribers.add(c);
} }
txn.setComplete(); txn.setComplete();
@@ -184,7 +447,8 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
Transaction txn = db.startTransaction(true); Transaction txn = db.startTransaction(true);
try { try {
for (Contact c : db.getContacts(txn)) { 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()); shared.add(c.getId());
} }
txn.setComplete(); txn.setComplete();
@@ -198,178 +462,417 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
} }
@Override @Override
public void setSharedWith(GroupId g, Collection<ContactId> shared) public boolean canBeShared(GroupId g, Contact c) throws DbException {
throws DbException { boolean canBeShared;
Transaction txn = db.startTransaction(true);
try { try {
Transaction txn = db.startTransaction(false); canBeShared = canBeShared(txn, g, c);
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);
}
}
txn.setComplete(); txn.setComplete();
} finally { } finally {
db.endTransaction(txn); 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) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);
} }
} }
private BdfDictionary initializeSharerState(Transaction txn, Forum f,
ContactId contactId) throws FormatException, DbException {
Contact c = db.getContact(txn, contactId);
Group group = getContactGroup(c);
// create local message to keep engine state
long now = clock.currentTimeMillis();
Bytes salt = new Bytes(new byte[FORUM_SALT_LENGTH]);
random.nextBytes(salt.getBytes());
Message m = clientHelper.createMessage(localGroup.getId(), now,
BdfList.of(salt));
MessageId sessionId = m.getId();
BdfDictionary d = new BdfDictionary();
d.put(SESSION_ID, sessionId);
d.put(STORAGE_ID, sessionId);
d.put(GROUP_ID, group.getId());
d.put(IS_SHARER, true);
d.put(STATE, PREPARE_INVITATION.getValue());
d.put(CONTACT_ID, contactId.getInt());
d.put(FORUM_ID, f.getId());
d.put(FORUM_NAME, f.getName());
d.put(FORUM_SALT, f.getSalt());
// save local state to database
clientHelper.addLocalMessage(txn, m, getClientId(), d, false);
return d;
}
private BdfDictionary initializeInviteeState(Transaction txn,
ContactId contactId, BdfDictionary msg)
throws FormatException, DbException {
Contact c = db.getContact(txn, contactId);
Group group = getContactGroup(c);
String name = msg.getString(FORUM_NAME);
byte[] salt = msg.getRaw(FORUM_SALT);
Forum f = forumManager.createForum(name, salt);
// create local message to keep engine state
long now = clock.currentTimeMillis();
Bytes mSalt = new Bytes(new byte[FORUM_SALT_LENGTH]);
random.nextBytes(mSalt.getBytes());
Message m = clientHelper.createMessage(localGroup.getId(), now,
BdfList.of(mSalt));
BdfDictionary d = new BdfDictionary();
d.put(SESSION_ID, msg.getRaw(SESSION_ID));
d.put(STORAGE_ID, m.getId());
d.put(GROUP_ID, group.getId());
d.put(IS_SHARER, false);
d.put(STATE, AWAIT_INVITATION.getValue());
d.put(CONTACT_ID, contactId.getInt());
d.put(FORUM_ID, f.getId());
d.put(FORUM_NAME, name);
d.put(FORUM_SALT, salt);
// save local state to database
clientHelper.addLocalMessage(txn, m, getClientId(), d, false);
return d;
}
private BdfDictionary getSessionState(Transaction txn, SessionId sessionId,
boolean warn) throws DbException, FormatException {
try {
// we should be able to get the sharer state directly from sessionId
return clientHelper.getMessageMetadataAsDictionary(txn, sessionId);
} catch (NoSuchMessageException e) {
// State not found directly, so iterate over all states
// to find state for invitee
Map<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 {
InviteeProtocolState state = InviteeProtocolState
.fromValue(d.getLong(STATE).intValue());
if (state == AWAIT_LOCAL_RESPONSE) {
byte[] id = d.getRaw(FORUM_ID);
if (Arrays.equals(f.getId().getBytes(), id)) {
// Note that there should always be only one session
// in this state for the same forum
return d;
}
}
} catch (FormatException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
throw new DbException();
}
private BdfDictionary getSessionStateForLeaving(Transaction txn, Forum f,
ContactId c) throws DbException, FormatException {
Map<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 void processStateUpdate(Transaction txn, MessageId messageId,
StateUpdate<BdfDictionary, BdfDictionary> result)
throws DbException, FormatException {
// perform actions based on new local state
performTasks(txn, result.localState);
// save new local state
MessageId storageId =
new MessageId(result.localState.getRaw(STORAGE_ID));
clientHelper.mergeMessageMetadata(txn, storageId, result.localState);
// send messages
for (BdfDictionary d : result.toSend) {
sendMessage(txn, d);
}
// broadcast events
for (Event event : result.toBroadcast) {
txn.attach(event);
}
// delete message
if (result.deleteMessage && messageId != null) {
if (LOG.isLoggable(INFO)) {
LOG.info("Deleting message with id " + messageId.hashCode());
}
db.deleteMessage(txn, messageId);
db.deleteMessageMetadata(txn, messageId);
}
}
private void performTasks(Transaction txn, BdfDictionary localState)
throws FormatException, DbException {
if (!localState.containsKey(TASK)) return;
// remember task and remove it from localState
long task = localState.getLong(TASK);
localState.put(TASK, BdfDictionary.NULL_VALUE);
// get group ID for later
GroupId groupId = new GroupId(localState.getRaw(GROUP_ID));
// get contact ID for later
ContactId contactId =
new ContactId(localState.getLong(CONTACT_ID).intValue());
// get forum for later
String name = localState.getString(FORUM_NAME);
byte[] salt = localState.getRaw(FORUM_SALT);
Forum f = forumManager.createForum(name, salt);
// perform tasks
if (task == TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US) {
addToList(txn, groupId, SHARED_WITH_US, f);
}
else if (task == TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US) {
removeFromList(txn, groupId, SHARED_WITH_US, f);
}
else if (task == TASK_ADD_SHARED_FORUM) {
db.addGroup(txn, f.getGroup());
db.setVisibleToContact(txn, contactId, f.getId(), true);
}
else if (task == TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US) {
addToList(txn, groupId, TO_BE_SHARED_BY_US, f);
}
else if (task == TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US) {
removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f);
}
else if (task == TASK_SHARE_FORUM) {
db.setVisibleToContact(txn, contactId, f.getId(), true);
removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f);
addToList(txn, groupId, SHARED_BY_US, f);
}
else if (task == TASK_UNSHARE_FORUM_SHARED_BY_US) {
db.setVisibleToContact(txn, contactId, f.getId(), false);
removeFromList(txn, groupId, SHARED_BY_US, f);
} else if (task == TASK_UNSHARE_FORUM_SHARED_WITH_US) {
db.setVisibleToContact(txn, contactId, f.getId(), false);
removeFromList(txn, groupId, SHARED_WITH_US, f);
}
}
private void sendMessage(Transaction txn, BdfDictionary m)
throws FormatException, DbException {
BdfList list = encodeMessage(m);
byte[] body = clientHelper.toByteArray(list);
GroupId groupId = new GroupId(m.getRaw(GROUP_ID));
Group group = db.getGroup(txn, groupId);
long timestamp = clock.currentTimeMillis();
// add message itself as metadata
m.put(LOCAL, true);
m.put(TIME, timestamp);
Metadata meta = metadataEncoder.encode(m);
messageQueueManager
.sendMessage(txn, group, timestamp, body, meta);
}
private BdfList encodeMessage(BdfDictionary m) throws FormatException {
long type = m.getLong(TYPE);
BdfList list;
if (type == SHARE_MSG_TYPE_INVITATION) {
list = BdfList.of(type,
m.getRaw(SESSION_ID),
m.getString(FORUM_NAME),
m.getRaw(FORUM_SALT)
);
String msg = m.getOptionalString(INVITATION_MSG);
if (msg != null) list.add(msg);
} else if (type == SHARE_MSG_TYPE_ACCEPT) {
list = BdfList.of(type, m.getRaw(SESSION_ID));
} else if (type == SHARE_MSG_TYPE_DECLINE) {
list = BdfList.of(type, m.getRaw(SESSION_ID));
} else if (type == SHARE_MSG_TYPE_LEAVE) {
list = BdfList.of(type, m.getRaw(SESSION_ID));
} else if (type == SHARE_MSG_TYPE_ABORT) {
list = BdfList.of(type, m.getRaw(SESSION_ID));
} else {
throw new FormatException();
}
return list;
}
private Group getContactGroup(Contact c) { private Group getContactGroup(Contact c) {
return privateGroupFactory.createPrivateGroup(CLIENT_ID, c); return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
} }
private LatestUpdate findLatest(Transaction txn, GroupId g, boolean local) private ContactId getContactId(Transaction txn, GroupId contactGroupId)
throws DbException, FormatException { throws DbException, FormatException {
LatestUpdate latest = null; BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn,
Map<MessageId, BdfDictionary> metadata = contactGroupId);
clientHelper.getMessageMetadataAsDictionary(txn, g); return new ContactId(meta.getLong(CONTACT_ID).intValue());
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);
}
return latest;
} }
private List<Forum> parseForumList(BdfList message) throws FormatException { private void leaveForum(Transaction txn, ContactId c, Forum f)
// Version, forum list throws DbException, FormatException {
BdfList forumList = message.getList(1);
List<Forum> forums = new ArrayList<Forum>(forumList.size()); BdfDictionary state = getSessionStateForLeaving(txn, f, c);
for (int i = 0; i < forumList.size(); i++) { BdfDictionary action = new BdfDictionary();
// Name, salt action.put(TYPE, SHARE_MSG_TYPE_LEAVE);
BdfList forum = forumList.getList(i); if (state.getBoolean(IS_SHARER)) {
SharerEngine engine = new SharerEngine();
processStateUpdate(txn, null,
engine.onLocalAction(state, action));
} else {
InviteeEngine engine = new InviteeEngine();
processStateUpdate(txn, null,
engine.onLocalAction(state, action));
}
}
private boolean listContains(Transaction txn, GroupId contactGroup,
GroupId forum, String key) throws DbException, FormatException {
List<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 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;
}
return false;
}
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 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 BdfList encodeForumList(List<Forum> forums) {
BdfList forumList = new BdfList();
for (Forum f : forums)
forumList.add(BdfList.of(f.getName(), f.getSalt()));
return forumList;
}
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 forums.add(forumManager
.createForum(forum.getString(0), forum.getRaw(1))); .createForum(forum.getString(0), forum.getRaw(1)));
} }
return forums; return forums;
} }
private void storeMessage(Transaction txn, GroupId g, List<Forum> forums, private void deleteMessage(Transaction txn, MessageId messageId)
long version) throws DbException { throws DbException {
try {
BdfList body = encodeForumList(forums, version); if (LOG.isLoggable(INFO))
long now = clock.currentTimeMillis(); LOG.info("Deleting message with ID: " + messageId.hashCode());
Message m = clientHelper.createMessage(g, now, body);
BdfDictionary meta = new BdfDictionary(); db.deleteMessage(txn, messageId);
meta.put("version", version); db.deleteMessageMetadata(txn, messageId);
meta.put("local", true);
clientHelper.addLocalMessage(txn, m, CLIENT_ID, meta, true);
} catch (FormatException e) {
throw new RuntimeException(e);
}
} }
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 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);
}
}
private Forum parseForum(Group g) throws FormatException {
byte[] descriptor = g.getDescriptor();
// Name, salt
BdfList forum = clientHelper.toList(descriptor, 0, descriptor.length);
return new Forum(g, forum.getString(0), forum.getRaw(1));
}
private boolean listContains(Transaction txn, GroupId g, GroupId forum,
boolean local) throws DbException, FormatException {
LatestUpdate latest = findLatest(txn, g, local);
if (latest == null) return false;
BdfList message = clientHelper.getMessageAsList(txn, latest.messageId);
List<Forum> list = parseForumList(message);
for (Forum f : list) if (f.getId().equals(forum)) return true;
return false;
}
private boolean addToList(Transaction txn, GroupId g, Forum f)
throws DbException, FormatException {
LatestUpdate latest = findLatest(txn, g, true);
if (latest == null) {
storeMessage(txn, g, Collections.singletonList(f), 0);
return true;
}
BdfList message = clientHelper.getMessageAsList(txn, latest.messageId);
List<Forum> list = parseForumList(message);
if (list.contains(f)) return false;
list.add(f);
storeMessage(txn, g, list, latest.version + 1);
return true;
}
private void removeFromList(Transaction txn, GroupId g, Forum f)
throws DbException, FormatException {
LatestUpdate latest = findLatest(txn, g, true);
if (latest == null) return;
BdfList message = clientHelper.getMessageAsList(txn, latest.messageId);
List<Forum> list = parseForumList(message);
if (list.remove(f)) storeMessage(txn, g, list, latest.version + 1);
}
private static class LatestUpdate {
private final MessageId messageId;
private final long version;
private LatestUpdate(MessageId messageId, long version) {
this.messageId = messageId;
this.version = version;
}
}
} }

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -3,6 +3,7 @@ package org.briarproject.introduction;
import org.briarproject.api.FormatException; import org.briarproject.api.FormatException;
import org.briarproject.api.clients.Client; import org.briarproject.api.clients.Client;
import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager.AddContactHook; import org.briarproject.api.contact.ContactManager.AddContactHook;
@@ -20,14 +21,12 @@ import org.briarproject.api.introduction.IntroductionManager;
import org.briarproject.api.introduction.IntroductionMessage; import org.briarproject.api.introduction.IntroductionMessage;
import org.briarproject.api.introduction.IntroductionRequest; import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.introduction.IntroductionResponse; import org.briarproject.api.introduction.IntroductionResponse;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageStatus; import org.briarproject.api.sync.MessageStatus;
import org.briarproject.api.system.Clock;
import org.briarproject.clients.BdfIncomingMessageHook; import org.briarproject.clients.BdfIncomingMessageHook;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
@@ -93,12 +92,11 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
@Inject @Inject
IntroductionManagerImpl(DatabaseComponent db, ClientHelper clientHelper, IntroductionManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
MetadataParser metadataParser, Clock clock, MetadataParser metadataParser, IntroducerManager introducerManager,
IntroducerManager introducerManager,
IntroduceeManager introduceeManager, IntroduceeManager introduceeManager,
IntroductionGroupFactory introductionGroupFactory) { IntroductionGroupFactory introductionGroupFactory) {
super(clientHelper, metadataParser, clock); super(clientHelper, metadataParser);
this.db = db; this.db = db;
this.introducerManager = introducerManager; this.introducerManager = introducerManager;
this.introduceeManager = introduceeManager; this.introduceeManager = introduceeManager;

View File

@@ -5,7 +5,7 @@ import org.junit.Test;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
public class ForumListValidatorTest extends BriarTestCase { public class ForumSharingValidatorTest extends BriarTestCase {
@Test @Test
public void testUnitTestsExist() { public void testUnitTestsExist() {

View File

@@ -118,7 +118,7 @@ public class IntroductionManagerImplTest extends BriarTestCase {
clock = context.mock(Clock.class); clock = context.mock(Clock.class);
introductionManager = new IntroductionManagerImpl( introductionManager = new IntroductionManagerImpl(
db, clientHelper, metadataParser, clock, introducerManager, db, clientHelper, metadataParser, introducerManager,
introduceeManager, introductionGroupFactory introduceeManager, introductionGroupFactory
); );
} }