mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-21 07:09:56 +01:00
Private group invitation protocol.
This commit is contained in:
@@ -27,7 +27,6 @@ import org.briarproject.api.privategroup.JoinMessageHeader;
|
|||||||
import org.briarproject.api.privategroup.PrivateGroup;
|
import org.briarproject.api.privategroup.PrivateGroup;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
|
||||||
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.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
@@ -94,8 +93,6 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
|
|||||||
PrivateGroupFactory privateGroupFactory;
|
PrivateGroupFactory privateGroupFactory;
|
||||||
@Inject
|
@Inject
|
||||||
GroupMessageFactory groupMessageFactory;
|
GroupMessageFactory groupMessageFactory;
|
||||||
@Inject
|
|
||||||
GroupInvitationManager groupInvitationManager;
|
|
||||||
|
|
||||||
// objects accessed from background threads need to be volatile
|
// objects accessed from background threads need to be volatile
|
||||||
private volatile Waiter validationWaiter;
|
private volatile Waiter validationWaiter;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.briarproject.identity.IdentityModule;
|
|||||||
import org.briarproject.lifecycle.LifecycleModule;
|
import org.briarproject.lifecycle.LifecycleModule;
|
||||||
import org.briarproject.messaging.MessagingModule;
|
import org.briarproject.messaging.MessagingModule;
|
||||||
import org.briarproject.privategroup.PrivateGroupModule;
|
import org.briarproject.privategroup.PrivateGroupModule;
|
||||||
|
import org.briarproject.privategroup.invitation.GroupInvitationModule;
|
||||||
import org.briarproject.properties.PropertiesModule;
|
import org.briarproject.properties.PropertiesModule;
|
||||||
import org.briarproject.sharing.SharingModule;
|
import org.briarproject.sharing.SharingModule;
|
||||||
import org.briarproject.sync.SyncModule;
|
import org.briarproject.sync.SyncModule;
|
||||||
@@ -40,6 +41,7 @@ import dagger.Component;
|
|||||||
EventModule.class,
|
EventModule.class,
|
||||||
MessagingModule.class,
|
MessagingModule.class,
|
||||||
PrivateGroupModule.class,
|
PrivateGroupModule.class,
|
||||||
|
GroupInvitationModule.class,
|
||||||
IdentityModule.class,
|
IdentityModule.class,
|
||||||
LifecycleModule.class,
|
LifecycleModule.class,
|
||||||
PropertiesModule.class,
|
PropertiesModule.class,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import org.briarproject.api.event.EventBus;
|
|||||||
import org.briarproject.api.feed.FeedManager;
|
import org.briarproject.api.feed.FeedManager;
|
||||||
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.identity.AuthorFactory;
|
|
||||||
import org.briarproject.api.identity.IdentityManager;
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
import org.briarproject.api.introduction.IntroductionManager;
|
import org.briarproject.api.introduction.IntroductionManager;
|
||||||
import org.briarproject.api.invitation.InvitationTaskFactory;
|
import org.briarproject.api.invitation.InvitationTaskFactory;
|
||||||
@@ -37,6 +36,7 @@ import org.briarproject.api.plugins.PluginManager;
|
|||||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
|
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||||
import org.briarproject.api.settings.SettingsManager;
|
import org.briarproject.api.settings.SettingsManager;
|
||||||
import org.briarproject.api.system.Clock;
|
import org.briarproject.api.system.Clock;
|
||||||
@@ -68,8 +68,6 @@ public interface AndroidComponent extends CoreEagerSingletons {
|
|||||||
|
|
||||||
DatabaseConfig databaseConfig();
|
DatabaseConfig databaseConfig();
|
||||||
|
|
||||||
AuthorFactory authFactory();
|
|
||||||
|
|
||||||
ReferenceManager referenceMangager();
|
ReferenceManager referenceMangager();
|
||||||
|
|
||||||
@DatabaseExecutor
|
@DatabaseExecutor
|
||||||
@@ -99,6 +97,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
|
|||||||
|
|
||||||
PrivateGroupManager privateGroupManager();
|
PrivateGroupManager privateGroupManager();
|
||||||
|
|
||||||
|
GroupInvitationFactory groupInvitationFactory();
|
||||||
|
|
||||||
GroupInvitationManager groupInvitationManager();
|
GroupInvitationManager groupInvitationManager();
|
||||||
|
|
||||||
PrivateGroupFactory privateGroupFactory();
|
PrivateGroupFactory privateGroupFactory();
|
||||||
|
|||||||
@@ -898,7 +898,7 @@ public class ConversationActivity extends BriarActivity
|
|||||||
@DatabaseExecutor
|
@DatabaseExecutor
|
||||||
private void respondToGroupRequest(SessionId id, boolean accept)
|
private void respondToGroupRequest(SessionId id, boolean accept)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
groupInvitationManager.respondToInvitation(id, accept);
|
groupInvitationManager.respondToInvitation(contactId, id, accept);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void introductionResponseError() {
|
private void introductionResponseError() {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.briarproject.android.privategroup.creation;
|
package org.briarproject.android.privategroup.creation;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.briarproject.R;
|
import org.briarproject.R;
|
||||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||||
@@ -16,7 +15,6 @@ import java.util.Collection;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static android.widget.Toast.LENGTH_SHORT;
|
|
||||||
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_INVITATION_MSG_LENGTH;
|
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_INVITATION_MSG_LENGTH;
|
||||||
|
|
||||||
public abstract class BaseGroupInviteActivity
|
public abstract class BaseGroupInviteActivity
|
||||||
@@ -68,9 +66,6 @@ public abstract class BaseGroupInviteActivity
|
|||||||
new UiResultExceptionHandler<Void, DbException>(this) {
|
new UiResultExceptionHandler<Void, DbException>(this) {
|
||||||
@Override
|
@Override
|
||||||
public void onResultUi(Void result) {
|
public void onResultUi(Void result) {
|
||||||
Toast.makeText(BaseGroupInviteActivity.this,
|
|
||||||
"Inviting members is not yet implemented",
|
|
||||||
LENGTH_SHORT).show();
|
|
||||||
setResult(RESULT_OK);
|
setResult(RESULT_OK);
|
||||||
supportFinishAfterTransition();
|
supportFinishAfterTransition();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import org.briarproject.android.controller.DbController;
|
|||||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||||
import org.briarproject.api.contact.ContactId;
|
import org.briarproject.api.contact.ContactId;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
public interface CreateGroupController extends DbController {
|
public interface CreateGroupController extends DbController {
|
||||||
|
|
||||||
void createGroup(String name,
|
void createGroup(String name,
|
||||||
|
|||||||
@@ -2,57 +2,75 @@ package org.briarproject.android.privategroup.creation;
|
|||||||
|
|
||||||
import org.briarproject.android.controller.DbControllerImpl;
|
import org.briarproject.android.controller.DbControllerImpl;
|
||||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||||
|
import org.briarproject.api.contact.Contact;
|
||||||
import org.briarproject.api.contact.ContactId;
|
import org.briarproject.api.contact.ContactId;
|
||||||
|
import org.briarproject.api.contact.ContactManager;
|
||||||
import org.briarproject.api.crypto.CryptoExecutor;
|
import org.briarproject.api.crypto.CryptoExecutor;
|
||||||
import org.briarproject.api.db.DatabaseExecutor;
|
import org.briarproject.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
|
import org.briarproject.api.db.NoSuchContactException;
|
||||||
import org.briarproject.api.identity.IdentityManager;
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
import org.briarproject.api.identity.LocalAuthor;
|
import org.briarproject.api.identity.LocalAuthor;
|
||||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.privategroup.GroupMessage;
|
import org.briarproject.api.privategroup.GroupMessage;
|
||||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||||
import org.briarproject.api.privategroup.PrivateGroup;
|
import org.briarproject.api.privategroup.PrivateGroup;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
|
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||||
|
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.system.Clock;
|
import org.briarproject.api.system.Clock;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
public class CreateGroupControllerImpl extends DbControllerImpl
|
public class CreateGroupControllerImpl extends DbControllerImpl
|
||||||
implements CreateGroupController {
|
implements CreateGroupController {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(CreateGroupControllerImpl.class.getName());
|
Logger.getLogger(CreateGroupControllerImpl.class.getName());
|
||||||
|
|
||||||
|
private final Executor cryptoExecutor;
|
||||||
|
private final ContactManager contactManager;
|
||||||
private final IdentityManager identityManager;
|
private final IdentityManager identityManager;
|
||||||
private final PrivateGroupFactory groupFactory;
|
private final PrivateGroupFactory groupFactory;
|
||||||
private final GroupMessageFactory groupMessageFactory;
|
private final GroupMessageFactory groupMessageFactory;
|
||||||
private final PrivateGroupManager groupManager;
|
private final PrivateGroupManager groupManager;
|
||||||
|
private final GroupInvitationFactory groupInvitationFactory;
|
||||||
|
private final GroupInvitationManager groupInvitationManager;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
@CryptoExecutor
|
|
||||||
private final Executor cryptoExecutor;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
@CryptoExecutor Executor cryptoExecutor,
|
@CryptoExecutor Executor cryptoExecutor,
|
||||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
LifecycleManager lifecycleManager, ContactManager contactManager,
|
||||||
PrivateGroupFactory groupFactory,
|
IdentityManager identityManager, PrivateGroupFactory groupFactory,
|
||||||
GroupMessageFactory groupMessageFactory,
|
GroupMessageFactory groupMessageFactory,
|
||||||
PrivateGroupManager groupManager, Clock clock) {
|
PrivateGroupManager groupManager,
|
||||||
|
GroupInvitationFactory groupInvitationFactory,
|
||||||
|
GroupInvitationManager groupInvitationManager, Clock clock) {
|
||||||
super(dbExecutor, lifecycleManager);
|
super(dbExecutor, lifecycleManager);
|
||||||
|
this.cryptoExecutor = cryptoExecutor;
|
||||||
|
this.contactManager = contactManager;
|
||||||
this.identityManager = identityManager;
|
this.identityManager = identityManager;
|
||||||
this.groupFactory = groupFactory;
|
this.groupFactory = groupFactory;
|
||||||
this.groupMessageFactory = groupMessageFactory;
|
this.groupMessageFactory = groupMessageFactory;
|
||||||
this.groupManager = groupManager;
|
this.groupManager = groupManager;
|
||||||
|
this.groupInvitationFactory = groupInvitationFactory;
|
||||||
|
this.groupInvitationManager = groupInvitationManager;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
this.cryptoExecutor = cryptoExecutor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -111,17 +129,89 @@ public class CreateGroupControllerImpl extends DbControllerImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendInvitation(final GroupId groupId,
|
public void sendInvitation(final GroupId g,
|
||||||
final Collection<ContactId> contacts, final String message,
|
final Collection<ContactId> contactIds, final String message,
|
||||||
final ResultExceptionHandler<Void, DbException> result) {
|
final ResultExceptionHandler<Void, DbException> handler) {
|
||||||
runOnDbThread(new Runnable() {
|
runOnDbThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// TODO actually send invitation
|
try {
|
||||||
//noinspection ConstantConditions
|
LocalAuthor localAuthor = identityManager.getLocalAuthor();
|
||||||
result.onResult(null);
|
List<Contact> contacts = new ArrayList<>();
|
||||||
|
for (ContactId c : contactIds) {
|
||||||
|
try {
|
||||||
|
contacts.add(contactManager.getContact(c));
|
||||||
|
} catch (NoSuchContactException e) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signInvitations(g, localAuthor, contacts, message, handler);
|
||||||
|
} catch (DbException e) {
|
||||||
|
if (LOG.isLoggable(WARNING))
|
||||||
|
LOG.log(WARNING, e.toString(), e);
|
||||||
|
handler.onException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void signInvitations(final GroupId g, final LocalAuthor localAuthor,
|
||||||
|
final Collection<Contact> contacts, final String message,
|
||||||
|
final ResultExceptionHandler<Void, DbException> handler) {
|
||||||
|
cryptoExecutor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
long timestamp = clock.currentTimeMillis();
|
||||||
|
List<InvitationContext> contexts = new ArrayList<>();
|
||||||
|
for (Contact c : contacts) {
|
||||||
|
byte[] signature = groupInvitationFactory.signInvitation(c,
|
||||||
|
g, timestamp, localAuthor.getPrivateKey());
|
||||||
|
contexts.add(new InvitationContext(c.getId(), timestamp,
|
||||||
|
signature));
|
||||||
|
}
|
||||||
|
sendInvitations(g, contexts, message, handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendInvitations(final GroupId g,
|
||||||
|
final Collection<InvitationContext> contexts, final String message,
|
||||||
|
final ResultExceptionHandler<Void, DbException> handler) {
|
||||||
|
runOnDbThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
String msg = message.isEmpty() ? null : message;
|
||||||
|
for (InvitationContext context : contexts) {
|
||||||
|
try {
|
||||||
|
groupInvitationManager.sendInvitation(g,
|
||||||
|
context.contactId, msg, context.timestamp,
|
||||||
|
context.signature);
|
||||||
|
} catch (NoSuchContactException e) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.onResult(null);
|
||||||
|
} catch (DbException e) {
|
||||||
|
if (LOG.isLoggable(WARNING))
|
||||||
|
LOG.log(WARNING, e.toString(), e);
|
||||||
|
handler.onException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class InvitationContext {
|
||||||
|
|
||||||
|
private final ContactId contactId;
|
||||||
|
private final long timestamp;
|
||||||
|
private final byte[] signature;
|
||||||
|
|
||||||
|
private InvitationContext(ContactId contactId, long timestamp,
|
||||||
|
byte[] signature) {
|
||||||
|
this.contactId = contactId;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,17 @@ import org.briarproject.android.sharing.ContactSelectorFragment;
|
|||||||
import org.briarproject.api.contact.Contact;
|
import org.briarproject.api.contact.Contact;
|
||||||
import org.briarproject.api.db.DatabaseExecutor;
|
import org.briarproject.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
|
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
public class GroupInviteActivity extends BaseGroupInviteActivity
|
public class GroupInviteActivity extends BaseGroupInviteActivity
|
||||||
implements MessageFragmentListener {
|
implements MessageFragmentListener {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
GroupInvitationManager groupInvitationManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void injectActivity(ActivityComponent component) {
|
public void injectActivity(ActivityComponent component) {
|
||||||
component.inject(this);
|
component.inject(this);
|
||||||
@@ -42,9 +48,8 @@ public class GroupInviteActivity extends BaseGroupInviteActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@DatabaseExecutor
|
@DatabaseExecutor
|
||||||
public boolean isDisabled(GroupId groupId, Contact c) throws DbException {
|
public boolean isDisabled(GroupId g, Contact c) throws DbException {
|
||||||
// TODO disable contacts that can not be invited
|
return !groupInvitationManager.isInvitationAllowed(c, g);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package org.briarproject.android.privategroup.invitation;
|
|||||||
|
|
||||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||||
import org.briarproject.android.sharing.InvitationControllerImpl;
|
import org.briarproject.android.sharing.InvitationControllerImpl;
|
||||||
import org.briarproject.api.contact.Contact;
|
import org.briarproject.api.contact.ContactId;
|
||||||
import org.briarproject.api.db.DatabaseExecutor;
|
import org.briarproject.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.event.Event;
|
import org.briarproject.api.event.Event;
|
||||||
@@ -70,8 +70,8 @@ public class GroupInvitationControllerImpl
|
|||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
PrivateGroup g = item.getShareable();
|
PrivateGroup g = item.getShareable();
|
||||||
Contact c = item.getCreator();
|
ContactId c = item.getCreator().getId();
|
||||||
groupInvitationManager.respondToInvitation(g, c, accept);
|
groupInvitationManager.respondToInvitation(c, g, accept);
|
||||||
} 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);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import org.briarproject.api.event.EventBus;
|
|||||||
import org.briarproject.api.event.EventListener;
|
import org.briarproject.api.event.EventListener;
|
||||||
import org.briarproject.api.event.GroupAddedEvent;
|
import org.briarproject.api.event.GroupAddedEvent;
|
||||||
import org.briarproject.api.event.GroupDissolvedEvent;
|
import org.briarproject.api.event.GroupDissolvedEvent;
|
||||||
import org.briarproject.api.event.GroupInvitationReceivedEvent;
|
import org.briarproject.api.event.GroupInvitationRequestReceivedEvent;
|
||||||
import org.briarproject.api.event.GroupMessageAddedEvent;
|
import org.briarproject.api.event.GroupMessageAddedEvent;
|
||||||
import org.briarproject.api.event.GroupRemovedEvent;
|
import org.briarproject.api.event.GroupRemovedEvent;
|
||||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||||
@@ -92,8 +92,7 @@ public class GroupListControllerImpl extends DbControllerImpl
|
|||||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||||
LOG.info("Private group message added");
|
LOG.info("Private group message added");
|
||||||
onGroupMessageAdded(g.getHeader());
|
onGroupMessageAdded(g.getHeader());
|
||||||
} else if (e instanceof GroupInvitationReceivedEvent) {
|
} else if (e instanceof GroupInvitationRequestReceivedEvent) {
|
||||||
GroupInvitationReceivedEvent g = (GroupInvitationReceivedEvent) e;
|
|
||||||
LOG.info("Private group invitation received");
|
LOG.info("Private group invitation received");
|
||||||
onGroupInvitationReceived();
|
onGroupInvitationReceived();
|
||||||
} else if (e instanceof GroupAddedEvent) {
|
} else if (e instanceof GroupAddedEvent) {
|
||||||
|
|||||||
@@ -14,38 +14,63 @@ import java.util.Collection;
|
|||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public interface PrivateGroupManager extends MessageTracker {
|
public interface PrivateGroupManager extends MessageTracker {
|
||||||
|
|
||||||
/** The unique ID of the private group client. */
|
/**
|
||||||
|
* The unique ID of the private group client.
|
||||||
|
*/
|
||||||
ClientId CLIENT_ID = new ClientId("org.briarproject.briar.privategroup");
|
ClientId CLIENT_ID = new ClientId("org.briarproject.briar.privategroup");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new private group and joins it.
|
* Adds a new private group and joins it.
|
||||||
*
|
*
|
||||||
* @param group The private group to add
|
* @param group The private group to add
|
||||||
* @param joinMsg The creator's own join message
|
* @param joinMsg The new member's join message
|
||||||
*/
|
*/
|
||||||
void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg)
|
void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
/** Removes a dissolved private group. */
|
/**
|
||||||
|
* Adds a new private group and joins it.
|
||||||
|
*
|
||||||
|
* @param group The private group to add
|
||||||
|
* @param joinMsg The new member's join message
|
||||||
|
*/
|
||||||
|
void addPrivateGroup(Transaction txn, PrivateGroup group,
|
||||||
|
GroupMessage joinMsg) throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a dissolved private group.
|
||||||
|
*/
|
||||||
void removePrivateGroup(GroupId g) throws DbException;
|
void removePrivateGroup(GroupId g) throws DbException;
|
||||||
|
|
||||||
/** Gets the MessageId of your previous message sent to the group */
|
/**
|
||||||
|
* Gets the MessageId of your previous message sent to the group
|
||||||
|
*/
|
||||||
MessageId getPreviousMsgId(GroupId g) throws DbException;
|
MessageId getPreviousMsgId(GroupId g) throws DbException;
|
||||||
|
|
||||||
/** Returns the timestamp of the message with the given ID */
|
/**
|
||||||
|
* Returns the timestamp of the message with the given ID
|
||||||
|
*/
|
||||||
// TODO change to getPreviousMessageHeader()
|
// TODO change to getPreviousMessageHeader()
|
||||||
long getMessageTimestamp(MessageId id) throws DbException;
|
long getMessageTimestamp(MessageId id) throws DbException;
|
||||||
|
|
||||||
/** Marks the group with GroupId g as resolved */
|
/**
|
||||||
|
* Marks the group with GroupId g as resolved
|
||||||
|
*/
|
||||||
void markGroupDissolved(Transaction txn, GroupId g) throws DbException;
|
void markGroupDissolved(Transaction txn, GroupId g) throws DbException;
|
||||||
|
|
||||||
/** Returns true if the private group has been dissolved. */
|
/**
|
||||||
|
* Returns true if the private group has been dissolved.
|
||||||
|
*/
|
||||||
boolean isDissolved(GroupId g) throws DbException;
|
boolean isDissolved(GroupId g) throws DbException;
|
||||||
|
|
||||||
/** Stores (and sends) a local group message. */
|
/**
|
||||||
|
* Stores (and sends) a local group message.
|
||||||
|
*/
|
||||||
GroupMessageHeader addLocalMessage(GroupMessage p) throws DbException;
|
GroupMessageHeader addLocalMessage(GroupMessage p) throws DbException;
|
||||||
|
|
||||||
/** Returns the private group with the given ID. */
|
/**
|
||||||
|
* Returns the private group with the given ID.
|
||||||
|
*/
|
||||||
PrivateGroup getPrivateGroup(GroupId g) throws DbException;
|
PrivateGroup getPrivateGroup(GroupId g) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,25 +78,35 @@ public interface PrivateGroupManager extends MessageTracker {
|
|||||||
*/
|
*/
|
||||||
PrivateGroup getPrivateGroup(Transaction txn, GroupId g) throws DbException;
|
PrivateGroup getPrivateGroup(Transaction txn, GroupId g) throws DbException;
|
||||||
|
|
||||||
/** Returns all private groups the user is a member of. */
|
/**
|
||||||
|
* Returns all private groups the user is a member of.
|
||||||
|
*/
|
||||||
Collection<PrivateGroup> getPrivateGroups() throws DbException;
|
Collection<PrivateGroup> getPrivateGroups() throws DbException;
|
||||||
|
|
||||||
/** Returns the body of the group message with the given ID. */
|
/**
|
||||||
|
* Returns the body of the group message with the given ID.
|
||||||
|
*/
|
||||||
String getMessageBody(MessageId m) throws DbException;
|
String getMessageBody(MessageId m) throws DbException;
|
||||||
|
|
||||||
/** Returns the headers of all group messages in the given group. */
|
/**
|
||||||
|
* Returns the headers of all group messages in the given group.
|
||||||
|
*/
|
||||||
Collection<GroupMessageHeader> getHeaders(GroupId g) throws DbException;
|
Collection<GroupMessageHeader> getHeaders(GroupId g) throws DbException;
|
||||||
|
|
||||||
/** Returns all members of the group with ID g */
|
/**
|
||||||
|
* Returns all members of the group with ID g
|
||||||
|
*/
|
||||||
Collection<GroupMember> getMembers(GroupId g) throws DbException;
|
Collection<GroupMember> getMembers(GroupId g) throws DbException;
|
||||||
|
|
||||||
/** Returns true if the given Author a is member of the group with ID g */
|
/**
|
||||||
|
* Returns true if the given Author a is member of the group with ID g
|
||||||
|
*/
|
||||||
boolean isMember(Transaction txn, GroupId g, Author a) throws DbException;
|
boolean isMember(Transaction txn, GroupId g, Author a) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a hook to be called when members are added
|
* Registers a hook to be called when members are added
|
||||||
* or groups are removed.
|
* or groups are removed.
|
||||||
* */
|
*/
|
||||||
void registerPrivateGroupHook(PrivateGroupHook hook);
|
void registerPrivateGroupHook(PrivateGroupHook hook);
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.briarproject.api.privategroup.invitation;
|
|
||||||
|
|
||||||
public interface GroupInvitationConstants {
|
|
||||||
|
|
||||||
// Group Metadata Keys
|
|
||||||
String CONTACT_ID = "contactId";
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.briarproject.api.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.contact.Contact;
|
||||||
|
import org.briarproject.api.crypto.CryptoExecutor;
|
||||||
|
import org.briarproject.api.data.BdfList;
|
||||||
|
import org.briarproject.api.identity.AuthorId;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
|
||||||
|
public interface GroupInvitationFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a signature to include when inviting a member to join a private
|
||||||
|
* group. If the member accepts the invitation, the signature will be
|
||||||
|
* included in the member's join message.
|
||||||
|
*/
|
||||||
|
@CryptoExecutor
|
||||||
|
byte[] signInvitation(Contact c, GroupId privateGroupId, long timestamp,
|
||||||
|
byte[] privateKey);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a token to be signed by the creator when inviting a member to
|
||||||
|
* join a private group. If the member accepts the invitation, the
|
||||||
|
* signature will be included in the member's join message.
|
||||||
|
*/
|
||||||
|
BdfList createInviteToken(AuthorId creatorId, AuthorId memberId,
|
||||||
|
GroupId privateGroupId, long timestamp);
|
||||||
|
}
|
||||||
@@ -13,10 +13,8 @@ public class GroupInvitationItem extends InvitationItem<PrivateGroup> {
|
|||||||
|
|
||||||
private final Contact creator;
|
private final Contact creator;
|
||||||
|
|
||||||
public GroupInvitationItem(PrivateGroup shareable, boolean subscribed,
|
public GroupInvitationItem(PrivateGroup privateGroup, Contact creator) {
|
||||||
Contact creator) {
|
super(privateGroup, false);
|
||||||
super(shareable, subscribed);
|
|
||||||
|
|
||||||
this.creator = creator;
|
this.creator = creator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package org.briarproject.api.privategroup.invitation;
|
package org.briarproject.api.privategroup.invitation;
|
||||||
|
|
||||||
import org.briarproject.api.clients.MessageTracker;
|
|
||||||
import org.briarproject.api.clients.SessionId;
|
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;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.privategroup.PrivateGroup;
|
import org.briarproject.api.privategroup.PrivateGroup;
|
||||||
import org.briarproject.api.sharing.InvitationMessage;
|
import org.briarproject.api.sharing.InvitationMessage;
|
||||||
import org.briarproject.api.sync.ClientId;
|
import org.briarproject.api.sync.ClientId;
|
||||||
@@ -12,38 +12,51 @@ import org.briarproject.api.sync.GroupId;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
public interface GroupInvitationManager extends MessageTracker {
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
/** The unique ID of the private group invitation client. */
|
@NotNullByDefault
|
||||||
|
public interface GroupInvitationManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique ID of the private group invitation client.
|
||||||
|
*/
|
||||||
ClientId CLIENT_ID =
|
ClientId CLIENT_ID =
|
||||||
new ClientId("org.briarproject.briar.privategroup.invitation");
|
new ClientId("org.briarproject.briar.privategroup.invitation");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an invitation to share the given forum with the given contact
|
* Sends an invitation to share the given private group with the given
|
||||||
* and sends an optional message along with it.
|
* contact, including an optional message.
|
||||||
*/
|
*/
|
||||||
void sendInvitation(GroupId groupId, ContactId contactId,
|
void sendInvitation(GroupId g, ContactId c, @Nullable String message,
|
||||||
String message) throws DbException;
|
long timestamp, byte[] signature) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responds to a pending private group invitation
|
* Responds to a pending private group invitation from the given contact.
|
||||||
*/
|
*/
|
||||||
void respondToInvitation(PrivateGroup g, Contact c, boolean accept)
|
void respondToInvitation(ContactId c, PrivateGroup g, boolean accept)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responds to a pending private group invitation
|
* Responds to a pending private group invitation from the given contact.
|
||||||
*/
|
*/
|
||||||
void respondToInvitation(SessionId id, boolean accept) throws DbException;
|
void respondToInvitation(ContactId c, SessionId s, boolean accept)
|
||||||
|
throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all private group invitation messages related to the contact
|
* Returns all private group invitation messages related to the given
|
||||||
* identified by contactId.
|
* contact.
|
||||||
*/
|
*/
|
||||||
Collection<InvitationMessage> getInvitationMessages(
|
Collection<InvitationMessage> getInvitationMessages(ContactId c)
|
||||||
ContactId contactId) throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
/** Returns all private groups to which the user has been invited. */
|
/**
|
||||||
|
* Returns all private groups to which the user has been invited.
|
||||||
|
*/
|
||||||
Collection<GroupInvitationItem> getInvitations() throws DbException;
|
Collection<GroupInvitationItem> getInvitations() throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given contact can be invited to the given private
|
||||||
|
* group.
|
||||||
|
*/
|
||||||
|
boolean isInvitationAllowed(Contact c, GroupId g) throws DbException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import org.briarproject.api.sharing.InvitationRequest;
|
|||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
import javax.annotation.concurrent.ThreadSafe;
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
@@ -19,8 +19,8 @@ public class GroupInvitationRequest extends InvitationRequest {
|
|||||||
private final Author creator;
|
private final Author creator;
|
||||||
|
|
||||||
public GroupInvitationRequest(MessageId id, SessionId sessionId,
|
public GroupInvitationRequest(MessageId id, SessionId sessionId,
|
||||||
GroupId groupId, Author creator, ContactId contactId,
|
GroupId groupId, ContactId contactId, @Nullable String message,
|
||||||
String groupName, String message, boolean available, long time,
|
String groupName, Author creator, boolean available, long time,
|
||||||
boolean local, boolean sent, boolean seen, boolean read) {
|
boolean local, boolean sent, boolean seen, boolean read) {
|
||||||
super(id, sessionId, groupId, contactId, message, available, time,
|
super(id, sessionId, groupId, contactId, message, available, time,
|
||||||
local, sent, seen, read);
|
local, sent, seen, read);
|
||||||
|
|||||||
@@ -2,39 +2,21 @@ package org.briarproject.api.privategroup.invitation;
|
|||||||
|
|
||||||
import org.briarproject.api.clients.SessionId;
|
import org.briarproject.api.clients.SessionId;
|
||||||
import org.briarproject.api.contact.ContactId;
|
import org.briarproject.api.contact.ContactId;
|
||||||
import org.briarproject.api.identity.Author;
|
|
||||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.sharing.InvitationResponse;
|
import org.briarproject.api.sharing.InvitationResponse;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
import javax.annotation.concurrent.ThreadSafe;
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class GroupInvitationResponse extends InvitationResponse {
|
public class GroupInvitationResponse extends InvitationResponse {
|
||||||
|
|
||||||
private final String groupName;
|
|
||||||
private final Author creator;
|
|
||||||
|
|
||||||
public GroupInvitationResponse(MessageId id, SessionId sessionId,
|
public GroupInvitationResponse(MessageId id, SessionId sessionId,
|
||||||
GroupId groupId, String groupName, Author creator,
|
GroupId groupId, ContactId contactId, boolean accept, long time,
|
||||||
ContactId contactId, boolean accept, long time, boolean local,
|
boolean local, boolean sent, boolean seen, boolean read) {
|
||||||
boolean sent, boolean seen, boolean read) {
|
|
||||||
super(id, sessionId, groupId, contactId, accept, time, local, sent,
|
super(id, sessionId, groupId, contactId, accept, time, local, sent,
|
||||||
seen, read);
|
seen, read);
|
||||||
this.groupName = groupName;
|
|
||||||
this.creator = creator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getGroupName() {
|
|
||||||
return groupName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Author getCreator() {
|
|
||||||
return creator;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package org.briarproject;
|
package org.briarproject;
|
||||||
|
|
||||||
import org.briarproject.api.privategroup.PrivateGroup;
|
|
||||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
|
||||||
import org.briarproject.blogs.BlogsModule;
|
import org.briarproject.blogs.BlogsModule;
|
||||||
import org.briarproject.contact.ContactModule;
|
import org.briarproject.contact.ContactModule;
|
||||||
import org.briarproject.crypto.CryptoModule;
|
import org.briarproject.crypto.CryptoModule;
|
||||||
@@ -14,6 +12,7 @@ import org.briarproject.lifecycle.LifecycleModule;
|
|||||||
import org.briarproject.messaging.MessagingModule;
|
import org.briarproject.messaging.MessagingModule;
|
||||||
import org.briarproject.plugins.PluginsModule;
|
import org.briarproject.plugins.PluginsModule;
|
||||||
import org.briarproject.privategroup.PrivateGroupModule;
|
import org.briarproject.privategroup.PrivateGroupModule;
|
||||||
|
import org.briarproject.privategroup.invitation.GroupInvitationModule;
|
||||||
import org.briarproject.properties.PropertiesModule;
|
import org.briarproject.properties.PropertiesModule;
|
||||||
import org.briarproject.sharing.SharingModule;
|
import org.briarproject.sharing.SharingModule;
|
||||||
import org.briarproject.sync.SyncModule;
|
import org.briarproject.sync.SyncModule;
|
||||||
@@ -32,6 +31,8 @@ public interface CoreEagerSingletons {
|
|||||||
|
|
||||||
void inject(ForumModule.EagerSingletons init);
|
void inject(ForumModule.EagerSingletons init);
|
||||||
|
|
||||||
|
void inject(GroupInvitationModule.EagerSingletons init);
|
||||||
|
|
||||||
void inject(IdentityModule.EagerSingletons init);
|
void inject(IdentityModule.EagerSingletons init);
|
||||||
|
|
||||||
void inject(IntroductionModule.EagerSingletons init);
|
void inject(IntroductionModule.EagerSingletons init);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.briarproject.lifecycle.LifecycleModule;
|
|||||||
import org.briarproject.messaging.MessagingModule;
|
import org.briarproject.messaging.MessagingModule;
|
||||||
import org.briarproject.plugins.PluginsModule;
|
import org.briarproject.plugins.PluginsModule;
|
||||||
import org.briarproject.privategroup.PrivateGroupModule;
|
import org.briarproject.privategroup.PrivateGroupModule;
|
||||||
|
import org.briarproject.privategroup.invitation.GroupInvitationModule;
|
||||||
import org.briarproject.properties.PropertiesModule;
|
import org.briarproject.properties.PropertiesModule;
|
||||||
import org.briarproject.reliability.ReliabilityModule;
|
import org.briarproject.reliability.ReliabilityModule;
|
||||||
import org.briarproject.reporting.ReportingModule;
|
import org.briarproject.reporting.ReportingModule;
|
||||||
@@ -40,6 +41,7 @@ import dagger.Module;
|
|||||||
DatabaseExecutorModule.class,
|
DatabaseExecutorModule.class,
|
||||||
EventModule.class,
|
EventModule.class,
|
||||||
ForumModule.class,
|
ForumModule.class,
|
||||||
|
GroupInvitationModule.class,
|
||||||
IdentityModule.class,
|
IdentityModule.class,
|
||||||
IntroductionModule.class,
|
IntroductionModule.class,
|
||||||
InvitationModule.class,
|
InvitationModule.class,
|
||||||
@@ -67,6 +69,7 @@ public class CoreModule {
|
|||||||
c.inject(new CryptoModule.EagerSingletons());
|
c.inject(new CryptoModule.EagerSingletons());
|
||||||
c.inject(new DatabaseExecutorModule.EagerSingletons());
|
c.inject(new DatabaseExecutorModule.EagerSingletons());
|
||||||
c.inject(new ForumModule.EagerSingletons());
|
c.inject(new ForumModule.EagerSingletons());
|
||||||
|
c.inject(new GroupInvitationModule.EagerSingletons());
|
||||||
c.inject(new IdentityModule.EagerSingletons());
|
c.inject(new IdentityModule.EagerSingletons());
|
||||||
c.inject(new LifecycleModule.EagerSingletons());
|
c.inject(new LifecycleModule.EagerSingletons());
|
||||||
c.inject(new MessagingModule.EagerSingletons());
|
c.inject(new MessagingModule.EagerSingletons());
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package org.briarproject.privategroup;
|
|||||||
import org.briarproject.api.FormatException;
|
import org.briarproject.api.FormatException;
|
||||||
import org.briarproject.api.clients.BdfMessageContext;
|
import org.briarproject.api.clients.BdfMessageContext;
|
||||||
import org.briarproject.api.clients.ClientHelper;
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
import org.briarproject.api.clients.ContactGroupFactory;
|
|
||||||
import org.briarproject.api.data.BdfDictionary;
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
import org.briarproject.api.data.BdfList;
|
import org.briarproject.api.data.BdfList;
|
||||||
import org.briarproject.api.data.MetadataEncoder;
|
import org.briarproject.api.data.MetadataEncoder;
|
||||||
@@ -12,6 +11,7 @@ import org.briarproject.api.identity.AuthorFactory;
|
|||||||
import org.briarproject.api.privategroup.MessageType;
|
import org.briarproject.api.privategroup.MessageType;
|
||||||
import org.briarproject.api.privategroup.PrivateGroup;
|
import org.briarproject.api.privategroup.PrivateGroup;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
|
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||||
import org.briarproject.api.sync.Group;
|
import org.briarproject.api.sync.Group;
|
||||||
import org.briarproject.api.sync.InvalidMessageException;
|
import org.briarproject.api.sync.InvalidMessageException;
|
||||||
import org.briarproject.api.sync.Message;
|
import org.briarproject.api.sync.Message;
|
||||||
@@ -29,7 +29,6 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH
|
|||||||
import static org.briarproject.api.privategroup.MessageType.JOIN;
|
import static org.briarproject.api.privategroup.MessageType.JOIN;
|
||||||
import static org.briarproject.api.privategroup.MessageType.POST;
|
import static org.briarproject.api.privategroup.MessageType.POST;
|
||||||
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
|
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
|
||||||
import static org.briarproject.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
|
|
||||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_ID;
|
import static org.briarproject.privategroup.Constants.KEY_MEMBER_ID;
|
||||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_NAME;
|
import static org.briarproject.privategroup.Constants.KEY_MEMBER_NAME;
|
||||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY;
|
import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY;
|
||||||
@@ -41,18 +40,18 @@ import static org.briarproject.privategroup.Constants.KEY_TYPE;
|
|||||||
|
|
||||||
class GroupMessageValidator extends BdfMessageValidator {
|
class GroupMessageValidator extends BdfMessageValidator {
|
||||||
|
|
||||||
private final ContactGroupFactory contactGroupFactory;
|
private final PrivateGroupFactory privateGroupFactory;
|
||||||
private final PrivateGroupFactory groupFactory;
|
|
||||||
private final AuthorFactory authorFactory;
|
private final AuthorFactory authorFactory;
|
||||||
|
private final GroupInvitationFactory groupInvitationFactory;
|
||||||
|
|
||||||
GroupMessageValidator(ContactGroupFactory contactGroupFactory,
|
GroupMessageValidator(PrivateGroupFactory privateGroupFactory,
|
||||||
PrivateGroupFactory groupFactory,
|
|
||||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||||
Clock clock, AuthorFactory authorFactory) {
|
Clock clock, AuthorFactory authorFactory,
|
||||||
|
GroupInvitationFactory groupInvitationFactory) {
|
||||||
super(clientHelper, metadataEncoder, clock);
|
super(clientHelper, metadataEncoder, clock);
|
||||||
this.contactGroupFactory = contactGroupFactory;
|
this.privateGroupFactory = privateGroupFactory;
|
||||||
this.groupFactory = groupFactory;
|
|
||||||
this.authorFactory = authorFactory;
|
this.authorFactory = authorFactory;
|
||||||
|
this.groupInvitationFactory = groupInvitationFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -96,15 +95,16 @@ class GroupMessageValidator extends BdfMessageValidator {
|
|||||||
|
|
||||||
// The content is a BDF list with five elements
|
// The content is a BDF list with five elements
|
||||||
checkSize(body, 5);
|
checkSize(body, 5);
|
||||||
PrivateGroup pg = groupFactory.parsePrivateGroup(g);
|
PrivateGroup pg = privateGroupFactory.parsePrivateGroup(g);
|
||||||
|
|
||||||
// invite is null if the member is the creator of the private group
|
// invite is null if the member is the creator of the private group
|
||||||
|
Author creator = pg.getAuthor();
|
||||||
BdfList invite = body.getOptionalList(3);
|
BdfList invite = body.getOptionalList(3);
|
||||||
if (invite == null) {
|
if (invite == null) {
|
||||||
if (!member.equals(pg.getAuthor()))
|
if (!member.equals(creator))
|
||||||
throw new InvalidMessageException();
|
throw new InvalidMessageException();
|
||||||
} else {
|
} else {
|
||||||
if (member.equals(pg.getAuthor()))
|
if (member.equals(creator))
|
||||||
throw new InvalidMessageException();
|
throw new InvalidMessageException();
|
||||||
|
|
||||||
// Otherwise invite is a list with two elements
|
// Otherwise invite is a list with two elements
|
||||||
@@ -120,21 +120,13 @@ class GroupMessageValidator extends BdfMessageValidator {
|
|||||||
byte[] creatorSignature = invite.getRaw(1);
|
byte[] creatorSignature = invite.getRaw(1);
|
||||||
checkLength(creatorSignature, 1, MAX_SIGNATURE_LENGTH);
|
checkLength(creatorSignature, 1, MAX_SIGNATURE_LENGTH);
|
||||||
|
|
||||||
// derive invitation group
|
// the invite token is signed by the creator of the private group
|
||||||
Group invitationGroup = contactGroupFactory
|
BdfList token = groupInvitationFactory
|
||||||
.createContactGroup(CLIENT_ID, pg.getAuthor().getId(),
|
.createInviteToken(creator.getId(), member.getId(),
|
||||||
member.getId());
|
pg.getId(), inviteTimestamp);
|
||||||
|
|
||||||
// signature with the creator's private key
|
|
||||||
// over a list with four elements:
|
|
||||||
// invite_type (int), invite_timestamp (int),
|
|
||||||
// invitation_group_id (raw), and private_group_id (raw)
|
|
||||||
BdfList signed =
|
|
||||||
BdfList.of(0, inviteTimestamp, invitationGroup.getId(),
|
|
||||||
g.getId());
|
|
||||||
try {
|
try {
|
||||||
clientHelper.verifySignature(creatorSignature,
|
clientHelper.verifySignature(creatorSignature,
|
||||||
pg.getAuthor().getPublicKey(), signed);
|
creator.getPublicKey(), token);
|
||||||
} catch (GeneralSecurityException e) {
|
} catch (GeneralSecurityException e) {
|
||||||
throw new InvalidMessageException(e);
|
throw new InvalidMessageException(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.briarproject.privategroup;
|
|||||||
|
|
||||||
import org.briarproject.api.FormatException;
|
import org.briarproject.api.FormatException;
|
||||||
import org.briarproject.api.clients.ClientHelper;
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
|
import org.briarproject.api.contact.Contact;
|
||||||
import org.briarproject.api.contact.ContactId;
|
import org.briarproject.api.contact.ContactId;
|
||||||
import org.briarproject.api.data.BdfDictionary;
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
import org.briarproject.api.data.BdfEntry;
|
import org.briarproject.api.data.BdfEntry;
|
||||||
@@ -86,6 +87,17 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
public void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg)
|
public void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
Transaction txn = db.startTransaction(false);
|
Transaction txn = db.startTransaction(false);
|
||||||
|
try {
|
||||||
|
addPrivateGroup(txn, group, joinMsg);
|
||||||
|
db.commitTransaction(txn);
|
||||||
|
} finally {
|
||||||
|
db.endTransaction(txn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addPrivateGroup(Transaction txn, PrivateGroup group,
|
||||||
|
GroupMessage joinMsg) throws DbException {
|
||||||
try {
|
try {
|
||||||
db.addGroup(txn, group.getGroup());
|
db.addGroup(txn, group.getGroup());
|
||||||
BdfDictionary meta = BdfDictionary.of(
|
BdfDictionary meta = BdfDictionary.of(
|
||||||
@@ -94,11 +106,8 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
);
|
);
|
||||||
clientHelper.mergeGroupMetadata(txn, group.getId(), meta);
|
clientHelper.mergeGroupMetadata(txn, group.getId(), meta);
|
||||||
joinPrivateGroup(txn, joinMsg);
|
joinPrivateGroup(txn, joinMsg);
|
||||||
db.commitTransaction(txn);
|
|
||||||
} catch (FormatException e) {
|
} catch (FormatException e) {
|
||||||
throw new DbException(e);
|
throw new DbException(e);
|
||||||
} finally {
|
|
||||||
db.endTransaction(txn);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +294,9 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
try {
|
try {
|
||||||
// type(0), member_name(1), member_public_key(2), parent_id(3),
|
// type(0), member_name(1), member_public_key(2), parent_id(3),
|
||||||
// previous_message_id(4), content(5), signature(6)
|
// previous_message_id(4), content(5), signature(6)
|
||||||
return clientHelper.getMessageAsList(m).getString(5);
|
BdfList body = clientHelper.getMessageAsList(m);
|
||||||
|
if (body == null) throw new DbException();
|
||||||
|
return body.getString(5);
|
||||||
} catch (FormatException e) {
|
} catch (FormatException e) {
|
||||||
throw new DbException(e);
|
throw new DbException(e);
|
||||||
}
|
}
|
||||||
@@ -370,10 +381,10 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
Status status = identityManager.getAuthorStatus(txn, a.getId());
|
Status status = identityManager.getAuthorStatus(txn, a.getId());
|
||||||
boolean shared = false;
|
boolean shared = false;
|
||||||
if (status == VERIFIED || status == UNVERIFIED) {
|
if (status == VERIFIED || status == UNVERIFIED) {
|
||||||
Collection<ContactId> contacts =
|
Collection<Contact> contacts =
|
||||||
db.getContacts(txn, a.getId());
|
db.getContactsByAuthorId(txn, a.getId());
|
||||||
if (contacts.size() != 1) throw new DbException();
|
if (contacts.size() != 1) throw new DbException();
|
||||||
ContactId c = contacts.iterator().next();
|
ContactId c = contacts.iterator().next().getId();
|
||||||
shared = db.isVisibleToContact(txn, c, g);
|
shared = db.isVisibleToContact(txn, c, g);
|
||||||
}
|
}
|
||||||
members.add(new GroupMember(a, status, shared));
|
members.add(new GroupMember(a, status, shared));
|
||||||
@@ -489,6 +500,9 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
new BdfEntry(KEY_MEMBER_PUBLIC_KEY, a.getPublicKey())
|
new BdfEntry(KEY_MEMBER_PUBLIC_KEY, a.getPublicKey())
|
||||||
));
|
));
|
||||||
clientHelper.mergeGroupMetadata(txn, g, meta);
|
clientHelper.mergeGroupMetadata(txn, g, meta);
|
||||||
|
for (PrivateGroupHook hook : hooks) {
|
||||||
|
hook.addingMember(txn, g, a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Author getAuthor(BdfDictionary meta) throws FormatException {
|
private Author getAuthor(BdfDictionary meta) throws FormatException {
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
package org.briarproject.privategroup;
|
package org.briarproject.privategroup;
|
||||||
|
|
||||||
import org.briarproject.api.clients.ClientHelper;
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
import org.briarproject.api.clients.ContactGroupFactory;
|
|
||||||
import org.briarproject.api.contact.ContactManager;
|
|
||||||
import org.briarproject.api.data.MetadataEncoder;
|
import org.briarproject.api.data.MetadataEncoder;
|
||||||
import org.briarproject.api.identity.AuthorFactory;
|
import org.briarproject.api.identity.AuthorFactory;
|
||||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
|
||||||
import org.briarproject.api.messaging.ConversationManager;
|
|
||||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||||
import org.briarproject.api.sync.ValidationManager;
|
import org.briarproject.api.sync.ValidationManager;
|
||||||
import org.briarproject.api.system.Clock;
|
import org.briarproject.api.system.Clock;
|
||||||
import org.briarproject.privategroup.invitation.GroupInvitationManagerImpl;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -21,6 +16,8 @@ import javax.inject.Singleton;
|
|||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
|
||||||
|
import static org.briarproject.api.privategroup.PrivateGroupManager.CLIENT_ID;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
public class PrivateGroupModule {
|
public class PrivateGroupModule {
|
||||||
|
|
||||||
@@ -28,19 +25,15 @@ public class PrivateGroupModule {
|
|||||||
@Inject
|
@Inject
|
||||||
GroupMessageValidator groupMessageValidator;
|
GroupMessageValidator groupMessageValidator;
|
||||||
@Inject
|
@Inject
|
||||||
GroupInvitationManager groupInvitationManager;
|
PrivateGroupManager groupManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
PrivateGroupManager provideForumManager(
|
PrivateGroupManager provideGroupManager(
|
||||||
PrivateGroupManagerImpl groupManager,
|
PrivateGroupManagerImpl groupManager,
|
||||||
ValidationManager validationManager) {
|
ValidationManager validationManager) {
|
||||||
|
validationManager.registerIncomingMessageHook(CLIENT_ID, groupManager);
|
||||||
validationManager
|
|
||||||
.registerIncomingMessageHook(PrivateGroupManager.CLIENT_ID,
|
|
||||||
groupManager);
|
|
||||||
|
|
||||||
return groupManager;
|
return groupManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,38 +52,16 @@ public class PrivateGroupModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
GroupMessageValidator provideGroupMessageValidator(
|
GroupMessageValidator provideGroupMessageValidator(
|
||||||
ContactGroupFactory contactGroupFactory,
|
PrivateGroupFactory privateGroupFactory,
|
||||||
PrivateGroupFactory groupFactory,
|
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||||
ValidationManager validationManager, ClientHelper clientHelper,
|
Clock clock, AuthorFactory authorFactory,
|
||||||
MetadataEncoder metadataEncoder, Clock clock,
|
GroupInvitationFactory groupInvitationFactory,
|
||||||
AuthorFactory authorFactory,
|
ValidationManager validationManager) {
|
||||||
GroupInvitationManager groupInvitationManager) {
|
|
||||||
|
|
||||||
GroupMessageValidator validator = new GroupMessageValidator(
|
GroupMessageValidator validator = new GroupMessageValidator(
|
||||||
contactGroupFactory, groupFactory, clientHelper,
|
privateGroupFactory, clientHelper, metadataEncoder, clock,
|
||||||
metadataEncoder, clock, authorFactory);
|
authorFactory, groupInvitationFactory);
|
||||||
validationManager.registerMessageValidator(
|
validationManager.registerMessageValidator(CLIENT_ID, validator);
|
||||||
PrivateGroupManager.CLIENT_ID, validator);
|
|
||||||
|
|
||||||
return validator;
|
return validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
GroupInvitationManager provideGroupInvitationManager(
|
|
||||||
LifecycleManager lifecycleManager, ContactManager contactManager,
|
|
||||||
GroupInvitationManagerImpl groupInvitationManager,
|
|
||||||
ConversationManager conversationManager,
|
|
||||||
ValidationManager validationManager) {
|
|
||||||
|
|
||||||
validationManager.registerIncomingMessageHook(
|
|
||||||
GroupInvitationManager.CLIENT_ID, groupInvitationManager);
|
|
||||||
lifecycleManager.registerClient(groupInvitationManager);
|
|
||||||
contactManager.registerAddContactHook(groupInvitationManager);
|
|
||||||
contactManager.registerRemoveContactHook(groupInvitationManager);
|
|
||||||
conversationManager.registerConversationClient(groupInvitationManager);
|
|
||||||
|
|
||||||
return groupInvitationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class AbortMessage extends GroupInvitationMessage {
|
||||||
|
|
||||||
|
AbortMessage(MessageId id, GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
long timestamp) {
|
||||||
|
super(id, contactGroupId, privateGroupId, timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
|
import org.briarproject.api.contact.ContactId;
|
||||||
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
|
import org.briarproject.api.data.BdfList;
|
||||||
|
import org.briarproject.api.db.DatabaseComponent;
|
||||||
|
import org.briarproject.api.db.DbException;
|
||||||
|
import org.briarproject.api.db.Transaction;
|
||||||
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
|
import org.briarproject.api.identity.LocalAuthor;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.privategroup.GroupMessage;
|
||||||
|
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroup;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
|
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.system.Clock;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.ABORT;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.INVITE;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.JOIN;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.LEAVE;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
abstract class AbstractProtocolEngine<S extends Session>
|
||||||
|
implements ProtocolEngine<S> {
|
||||||
|
|
||||||
|
protected final DatabaseComponent db;
|
||||||
|
protected final ClientHelper clientHelper;
|
||||||
|
protected final PrivateGroupManager privateGroupManager;
|
||||||
|
|
||||||
|
private final IdentityManager identityManager;
|
||||||
|
private final PrivateGroupFactory privateGroupFactory;
|
||||||
|
private final GroupMessageFactory groupMessageFactory;
|
||||||
|
private final MessageParser messageParser;
|
||||||
|
private final MessageEncoder messageEncoder;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
AbstractProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
|
||||||
|
PrivateGroupManager privateGroupManager,
|
||||||
|
PrivateGroupFactory privateGroupFactory,
|
||||||
|
GroupMessageFactory groupMessageFactory,
|
||||||
|
IdentityManager identityManager, MessageParser messageParser,
|
||||||
|
MessageEncoder messageEncoder, Clock clock) {
|
||||||
|
this.db = db;
|
||||||
|
this.clientHelper = clientHelper;
|
||||||
|
this.privateGroupManager = privateGroupManager;
|
||||||
|
this.privateGroupFactory = privateGroupFactory;
|
||||||
|
this.groupMessageFactory = groupMessageFactory;
|
||||||
|
this.identityManager = identityManager;
|
||||||
|
this.messageParser = messageParser;
|
||||||
|
this.messageEncoder = messageEncoder;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContactId getContactId(Transaction txn, GroupId contactGroupId)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn,
|
||||||
|
contactGroupId);
|
||||||
|
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSubscribedPrivateGroup(Transaction txn, GroupId g)
|
||||||
|
throws DbException {
|
||||||
|
if (!db.containsGroup(txn, g)) return false;
|
||||||
|
Group group = db.getGroup(txn, g);
|
||||||
|
return group.getClientId().equals(PrivateGroupManager.CLIENT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isValidDependency(S session, @Nullable MessageId dependency) {
|
||||||
|
MessageId expected = session.getLastRemoteMessageId();
|
||||||
|
if (dependency == null) return expected == null;
|
||||||
|
return dependency.equals(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
void syncPrivateGroupWithContact(Transaction txn, S session, boolean sync)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
ContactId contactId = getContactId(txn, session.getContactGroupId());
|
||||||
|
db.setVisibleToContact(txn, contactId, session.getPrivateGroupId(),
|
||||||
|
sync);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message sendInviteMessage(Transaction txn, S session,
|
||||||
|
@Nullable String message, long timestamp, byte[] signature)
|
||||||
|
throws DbException {
|
||||||
|
Group g = db.getGroup(txn, session.getPrivateGroupId());
|
||||||
|
PrivateGroup privateGroup;
|
||||||
|
try {
|
||||||
|
privateGroup = privateGroupFactory.parsePrivateGroup(g);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e); // Invalid group descriptor
|
||||||
|
}
|
||||||
|
Message m = messageEncoder.encodeInviteMessage(
|
||||||
|
session.getContactGroupId(), privateGroup.getId(),
|
||||||
|
timestamp, privateGroup.getName(), privateGroup.getAuthor(),
|
||||||
|
privateGroup.getSalt(), message, signature);
|
||||||
|
sendMessage(txn, m, INVITE, privateGroup.getId(), true);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message sendJoinMessage(Transaction txn, S session, boolean visibleInUi)
|
||||||
|
throws DbException {
|
||||||
|
Message m = messageEncoder.encodeJoinMessage(
|
||||||
|
session.getContactGroupId(), session.getPrivateGroupId(),
|
||||||
|
getLocalTimestamp(session), session.getLastLocalMessageId());
|
||||||
|
sendMessage(txn, m, JOIN, session.getPrivateGroupId(), visibleInUi);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message sendLeaveMessage(Transaction txn, S session, boolean visibleInUi)
|
||||||
|
throws DbException {
|
||||||
|
Message m = messageEncoder.encodeLeaveMessage(
|
||||||
|
session.getContactGroupId(), session.getPrivateGroupId(),
|
||||||
|
getLocalTimestamp(session), session.getLastLocalMessageId());
|
||||||
|
sendMessage(txn, m, LEAVE, session.getPrivateGroupId(), visibleInUi);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message sendAbortMessage(Transaction txn, S session) throws DbException {
|
||||||
|
Message m = messageEncoder.encodeAbortMessage(
|
||||||
|
session.getContactGroupId(), session.getPrivateGroupId(),
|
||||||
|
getLocalTimestamp(session));
|
||||||
|
sendMessage(txn, m, ABORT, session.getPrivateGroupId(), false);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
void markMessageVisibleInUi(Transaction txn, MessageId m, boolean visible)
|
||||||
|
throws DbException {
|
||||||
|
BdfDictionary meta = new BdfDictionary();
|
||||||
|
messageEncoder.setVisibleInUi(meta, visible);
|
||||||
|
try {
|
||||||
|
clientHelper.mergeMessageMetadata(txn, m, meta);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void markMessageAvailableToAnswer(Transaction txn, MessageId m,
|
||||||
|
boolean available) throws DbException {
|
||||||
|
BdfDictionary meta = new BdfDictionary();
|
||||||
|
messageEncoder.setAvailableToAnswer(meta, available);
|
||||||
|
try {
|
||||||
|
clientHelper.mergeMessageMetadata(txn, m, meta);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void markInvitesUnavailableToAnswer(Transaction txn, S session)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
GroupId privateGroupId = session.getPrivateGroupId();
|
||||||
|
BdfDictionary query =
|
||||||
|
messageParser.getInvitesAvailableToAnswerQuery(privateGroupId);
|
||||||
|
Map<MessageId, BdfDictionary> results =
|
||||||
|
clientHelper.getMessageMetadataAsDictionary(txn,
|
||||||
|
session.getContactGroupId(), query);
|
||||||
|
for (MessageId m : results.keySet())
|
||||||
|
markMessageAvailableToAnswer(txn, m, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void subscribeToPrivateGroup(Transaction txn, MessageId inviteId)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
InviteMessage invite = getInviteMessage(txn, inviteId);
|
||||||
|
PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
|
||||||
|
invite.getGroupName(), invite.getCreator(), invite.getSalt());
|
||||||
|
long timestamp =
|
||||||
|
Math.max(clock.currentTimeMillis(), invite.getTimestamp() + 1);
|
||||||
|
// TODO: Create the join message on the crypto executor
|
||||||
|
LocalAuthor member = identityManager.getLocalAuthor(txn);
|
||||||
|
GroupMessage joinMessage = groupMessageFactory.createJoinMessage(
|
||||||
|
privateGroup.getId(), timestamp, member, invite.getTimestamp(),
|
||||||
|
invite.getSignature());
|
||||||
|
privateGroupManager.addPrivateGroup(txn, privateGroup, joinMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
long getLocalTimestamp(S session) {
|
||||||
|
return Math.max(clock.currentTimeMillis(),
|
||||||
|
Math.max(session.getLocalTimestamp(),
|
||||||
|
session.getInviteTimestamp()) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InviteMessage getInviteMessage(Transaction txn, MessageId m)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
Message message = clientHelper.getMessage(txn, m);
|
||||||
|
if (message == null) throw new DbException();
|
||||||
|
BdfList body = clientHelper.toList(message);
|
||||||
|
return messageParser.parseInviteMessage(message, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMessage(Transaction txn, Message m, MessageType type,
|
||||||
|
GroupId privateGroupId, boolean visibleInConversation)
|
||||||
|
throws DbException {
|
||||||
|
BdfDictionary meta = messageEncoder.encodeMetadata(type, privateGroupId,
|
||||||
|
m.getTimestamp(), true, true, visibleInConversation, false);
|
||||||
|
try {
|
||||||
|
clientHelper.addLocalMessage(txn, m, meta, true);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
|
import org.briarproject.api.clients.ProtocolStateException;
|
||||||
|
import org.briarproject.api.db.DatabaseComponent;
|
||||||
|
import org.briarproject.api.db.DbException;
|
||||||
|
import org.briarproject.api.db.Transaction;
|
||||||
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
|
import org.briarproject.api.sync.Message;
|
||||||
|
import org.briarproject.api.system.Clock;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
import static org.briarproject.privategroup.invitation.CreatorState.DISSOLVED;
|
||||||
|
import static org.briarproject.privategroup.invitation.CreatorState.ERROR;
|
||||||
|
import static org.briarproject.privategroup.invitation.CreatorState.INVITED;
|
||||||
|
import static org.briarproject.privategroup.invitation.CreatorState.INVITEE_JOINED;
|
||||||
|
import static org.briarproject.privategroup.invitation.CreatorState.INVITEE_LEFT;
|
||||||
|
import static org.briarproject.privategroup.invitation.CreatorState.START;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class CreatorProtocolEngine extends AbstractProtocolEngine<CreatorSession> {
|
||||||
|
|
||||||
|
CreatorProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
|
||||||
|
PrivateGroupManager privateGroupManager,
|
||||||
|
PrivateGroupFactory privateGroupFactory,
|
||||||
|
GroupMessageFactory groupMessageFactory,
|
||||||
|
IdentityManager identityManager, MessageParser messageParser,
|
||||||
|
MessageEncoder messageEncoder, Clock clock) {
|
||||||
|
super(db, clientHelper, privateGroupManager, privateGroupFactory,
|
||||||
|
groupMessageFactory, identityManager,messageParser,
|
||||||
|
messageEncoder, clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CreatorSession onInviteAction(Transaction txn, CreatorSession s,
|
||||||
|
@Nullable String message, long timestamp, byte[] signature)
|
||||||
|
throws DbException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
return onLocalInvite(txn, s, message, timestamp, signature);
|
||||||
|
case INVITED:
|
||||||
|
case INVITEE_JOINED:
|
||||||
|
case INVITEE_LEFT:
|
||||||
|
case DISSOLVED:
|
||||||
|
case ERROR:
|
||||||
|
throw new ProtocolStateException(); // Invalid in these states
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CreatorSession onJoinAction(Transaction txn, CreatorSession s)
|
||||||
|
throws DbException {
|
||||||
|
throw new UnsupportedOperationException(); // Invalid in this role
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CreatorSession onLeaveAction(Transaction txn, CreatorSession s)
|
||||||
|
throws DbException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
case DISSOLVED:
|
||||||
|
case ERROR:
|
||||||
|
return s; // Ignored in these states
|
||||||
|
case INVITED:
|
||||||
|
case INVITEE_JOINED:
|
||||||
|
case INVITEE_LEFT:
|
||||||
|
return onLocalLeave(txn, s);
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CreatorSession onMemberAddedAction(Transaction txn, CreatorSession s)
|
||||||
|
throws DbException {
|
||||||
|
return s; // Ignored in this role
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CreatorSession onInviteMessage(Transaction txn, CreatorSession s,
|
||||||
|
InviteMessage m) throws DbException, FormatException {
|
||||||
|
return abort(txn, s); // Invalid in this role
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CreatorSession onJoinMessage(Transaction txn, CreatorSession s,
|
||||||
|
JoinMessage m) throws DbException, FormatException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
case INVITEE_JOINED:
|
||||||
|
case INVITEE_LEFT:
|
||||||
|
return abort(txn, s); // Invalid in these states
|
||||||
|
case INVITED:
|
||||||
|
return onRemoteAccept(txn, s, m);
|
||||||
|
case DISSOLVED:
|
||||||
|
case ERROR:
|
||||||
|
return s; // Ignored in these states
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CreatorSession onLeaveMessage(Transaction txn, CreatorSession s,
|
||||||
|
LeaveMessage m) throws DbException, FormatException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
case INVITEE_LEFT:
|
||||||
|
return abort(txn, s); // Invalid in these states
|
||||||
|
case INVITED:
|
||||||
|
return onRemoteDecline(txn, s, m);
|
||||||
|
case INVITEE_JOINED:
|
||||||
|
return onRemoteLeave(txn, s, m);
|
||||||
|
case DISSOLVED:
|
||||||
|
case ERROR:
|
||||||
|
return s; // Ignored in these states
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CreatorSession onAbortMessage(Transaction txn, CreatorSession s,
|
||||||
|
AbortMessage m) throws DbException, FormatException {
|
||||||
|
return abort(txn, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CreatorSession onLocalInvite(Transaction txn, CreatorSession s,
|
||||||
|
@Nullable String message, long timestamp, byte[] signature)
|
||||||
|
throws DbException {
|
||||||
|
// Send an INVITE message
|
||||||
|
Message sent = sendInviteMessage(txn, s, message, timestamp, signature);
|
||||||
|
long localTimestamp = Math.max(timestamp, getLocalTimestamp(s));
|
||||||
|
// Move to the INVITED state
|
||||||
|
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), localTimestamp,
|
||||||
|
timestamp, INVITED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CreatorSession onLocalLeave(Transaction txn, CreatorSession s)
|
||||||
|
throws DbException {
|
||||||
|
try {
|
||||||
|
// Stop syncing the private group with the contact
|
||||||
|
syncPrivateGroupWithContact(txn, s, false);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e); // Invalid group metadata
|
||||||
|
}
|
||||||
|
// Send a LEAVE message
|
||||||
|
Message sent = sendLeaveMessage(txn, s, false);
|
||||||
|
// Move to the DISSOLVED state
|
||||||
|
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||||
|
s.getInviteTimestamp(), DISSOLVED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CreatorSession onRemoteAccept(Transaction txn, CreatorSession s,
|
||||||
|
JoinMessage m) throws DbException, FormatException {
|
||||||
|
// The timestamp must be higher than the last invite message
|
||||||
|
if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
|
||||||
|
// The dependency, if any, must be the last remote message
|
||||||
|
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||||
|
return abort(txn, s);
|
||||||
|
// Mark the response visible
|
||||||
|
markMessageVisibleInUi(txn, m.getId(), true);
|
||||||
|
// Start syncing the private group with the contact
|
||||||
|
syncPrivateGroupWithContact(txn, s, true);
|
||||||
|
// Move to the INVITEE_JOINED state
|
||||||
|
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||||
|
s.getInviteTimestamp(), INVITEE_JOINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CreatorSession onRemoteDecline(Transaction txn, CreatorSession s,
|
||||||
|
LeaveMessage m) throws DbException, FormatException {
|
||||||
|
// The timestamp must be higher than the last invite message
|
||||||
|
if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
|
||||||
|
// The dependency, if any, must be the last remote message
|
||||||
|
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||||
|
return abort(txn, s);
|
||||||
|
// Mark the response visible
|
||||||
|
markMessageVisibleInUi(txn, m.getId(), true);
|
||||||
|
// Move to the START state
|
||||||
|
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||||
|
s.getInviteTimestamp(), START);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CreatorSession onRemoteLeave(Transaction txn, CreatorSession s,
|
||||||
|
LeaveMessage m) throws DbException, FormatException {
|
||||||
|
// The timestamp must be higher than the last invite message
|
||||||
|
if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
|
||||||
|
// The dependency, if any, must be the last remote message
|
||||||
|
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||||
|
return abort(txn, s);
|
||||||
|
// Stop syncing the private group with the contact
|
||||||
|
syncPrivateGroupWithContact(txn, s, false);
|
||||||
|
// Move to the INVITEE_LEFT state
|
||||||
|
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||||
|
s.getInviteTimestamp(), INVITEE_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CreatorSession abort(Transaction txn, CreatorSession s)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
// If the session has already been aborted, do nothing
|
||||||
|
if (s.getState() == ERROR) return s;
|
||||||
|
// If we subscribe, stop syncing the private group with the contact
|
||||||
|
if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId()))
|
||||||
|
syncPrivateGroupWithContact(txn, s, false);
|
||||||
|
// Send an ABORT message
|
||||||
|
Message sent = sendAbortMessage(txn, s);
|
||||||
|
// Move to the ERROR state
|
||||||
|
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||||
|
s.getInviteTimestamp(), ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
import static org.briarproject.privategroup.invitation.CreatorState.START;
|
||||||
|
import static org.briarproject.privategroup.invitation.Role.CREATOR;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class CreatorSession extends Session<CreatorState> {
|
||||||
|
|
||||||
|
private final CreatorState state;
|
||||||
|
|
||||||
|
CreatorSession(GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
@Nullable MessageId lastLocalMessageId,
|
||||||
|
@Nullable MessageId lastRemoteMessageId, long localTimestamp,
|
||||||
|
long inviteTimestamp, CreatorState state) {
|
||||||
|
super(contactGroupId, privateGroupId, lastLocalMessageId,
|
||||||
|
lastRemoteMessageId, localTimestamp, inviteTimestamp);
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatorSession(GroupId contactGroupId, GroupId privateGroupId) {
|
||||||
|
this(contactGroupId, privateGroupId, null, null, 0, 0, START);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Role getRole() {
|
||||||
|
return CREATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
CreatorState getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
|
||||||
|
enum CreatorState implements State {
|
||||||
|
|
||||||
|
START(0), INVITED(1), INVITEE_JOINED(2), INVITEE_LEFT(3), DISSOLVED(4),
|
||||||
|
ERROR(5);
|
||||||
|
|
||||||
|
private final int value;
|
||||||
|
|
||||||
|
CreatorState(int value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CreatorState fromValue(int value) throws FormatException {
|
||||||
|
for (CreatorState s : values()) if (s.value == value) return s;
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
interface GroupInvitationConstants {
|
||||||
|
|
||||||
|
// Group metadata keys
|
||||||
|
String GROUP_KEY_CONTACT_ID = "contactId";
|
||||||
|
|
||||||
|
// Message metadata keys
|
||||||
|
String MSG_KEY_MESSAGE_TYPE = "messageType";
|
||||||
|
String MSG_KEY_PRIVATE_GROUP_ID = "privateGroupId";
|
||||||
|
String MSG_KEY_TIMESTAMP = "timestamp";
|
||||||
|
String MSG_KEY_LOCAL = "local";
|
||||||
|
String MSG_KEY_VISIBLE_IN_UI = "visibleInUi";
|
||||||
|
String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer";
|
||||||
|
|
||||||
|
// Session keys
|
||||||
|
String SESSION_KEY_SESSION_ID = "sessionId";
|
||||||
|
String SESSION_KEY_PRIVATE_GROUP_ID = "privateGroupId";
|
||||||
|
String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId";
|
||||||
|
String SESSION_KEY_LAST_REMOTE_MESSAGE_ID = "lastRemoteMessageId";
|
||||||
|
String SESSION_KEY_LOCAL_TIMESTAMP = "localTimestamp";
|
||||||
|
String SESSION_KEY_INVITE_TIMESTAMP = "inviteTimestamp";
|
||||||
|
String SESSION_KEY_ROLE = "role";
|
||||||
|
String SESSION_KEY_STATE = "state";
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
|
import org.briarproject.api.clients.ContactGroupFactory;
|
||||||
|
import org.briarproject.api.contact.Contact;
|
||||||
|
import org.briarproject.api.data.BdfList;
|
||||||
|
import org.briarproject.api.identity.AuthorId;
|
||||||
|
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||||
|
import org.briarproject.api.sync.Group;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static org.briarproject.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
|
||||||
|
|
||||||
|
class GroupInvitationFactoryImpl implements GroupInvitationFactory {
|
||||||
|
|
||||||
|
private final ContactGroupFactory contactGroupFactory;
|
||||||
|
private final ClientHelper clientHelper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
GroupInvitationFactoryImpl(ContactGroupFactory contactGroupFactory,
|
||||||
|
ClientHelper clientHelper) {
|
||||||
|
this.contactGroupFactory = contactGroupFactory;
|
||||||
|
this.clientHelper = clientHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] signInvitation(Contact c, GroupId privateGroupId,
|
||||||
|
long timestamp, byte[] privateKey) {
|
||||||
|
AuthorId creatorId = c.getLocalAuthorId();
|
||||||
|
AuthorId memberId = c.getAuthor().getId();
|
||||||
|
BdfList token = createInviteToken(creatorId, memberId, privateGroupId,
|
||||||
|
timestamp);
|
||||||
|
try {
|
||||||
|
return clientHelper.sign(token, privateKey);
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BdfList createInviteToken(AuthorId creatorId, AuthorId memberId,
|
||||||
|
GroupId privateGroupId, long timestamp) {
|
||||||
|
Group contactGroup = contactGroupFactory.createContactGroup(CLIENT_ID,
|
||||||
|
creatorId, memberId);
|
||||||
|
return BdfList.of(
|
||||||
|
0, // TODO: Replace with a namespaced string
|
||||||
|
timestamp,
|
||||||
|
contactGroup.getId(),
|
||||||
|
privateGroupId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,44 +7,89 @@ import org.briarproject.api.clients.ContactGroupFactory;
|
|||||||
import org.briarproject.api.clients.SessionId;
|
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;
|
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.BdfDictionary;
|
||||||
import org.briarproject.api.data.BdfList;
|
import org.briarproject.api.data.BdfList;
|
||||||
import org.briarproject.api.data.MetadataParser;
|
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.Transaction;
|
import org.briarproject.api.db.Transaction;
|
||||||
import org.briarproject.api.messaging.ConversationManager;
|
import org.briarproject.api.identity.Author;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.privategroup.PrivateGroup;
|
import org.briarproject.api.privategroup.PrivateGroup;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupManager.PrivateGroupHook;
|
||||||
import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
|
import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
|
||||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||||
|
import org.briarproject.api.privategroup.invitation.GroupInvitationRequest;
|
||||||
|
import org.briarproject.api.privategroup.invitation.GroupInvitationResponse;
|
||||||
import org.briarproject.api.sharing.InvitationMessage;
|
import org.briarproject.api.sharing.InvitationMessage;
|
||||||
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.MessageStatus;
|
||||||
import org.briarproject.clients.ConversationClientImpl;
|
import org.briarproject.clients.ConversationClientImpl;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static org.briarproject.api.privategroup.invitation.GroupInvitationConstants.CONTACT_ID;
|
import static org.briarproject.privategroup.invitation.CreatorState.START;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.ABORT;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.INVITE;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.JOIN;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.LEAVE;
|
||||||
|
import static org.briarproject.privategroup.invitation.Role.CREATOR;
|
||||||
|
import static org.briarproject.privategroup.invitation.Role.INVITEE;
|
||||||
|
import static org.briarproject.privategroup.invitation.Role.PEER;
|
||||||
|
|
||||||
public class GroupInvitationManagerImpl extends ConversationClientImpl
|
@Immutable
|
||||||
implements GroupInvitationManager, Client,
|
@NotNullByDefault
|
||||||
ContactManager.AddContactHook, ContactManager.RemoveContactHook,
|
class GroupInvitationManagerImpl extends ConversationClientImpl
|
||||||
ConversationManager.ConversationClient {
|
implements GroupInvitationManager, Client, AddContactHook,
|
||||||
|
RemoveContactHook, PrivateGroupHook {
|
||||||
|
|
||||||
private final ContactGroupFactory contactGroupFactory;
|
private final ContactGroupFactory contactGroupFactory;
|
||||||
|
private final PrivateGroupFactory privateGroupFactory;
|
||||||
|
private final PrivateGroupManager privateGroupManager;
|
||||||
|
private final MessageParser messageParser;
|
||||||
|
private final SessionParser sessionParser;
|
||||||
|
private final SessionEncoder sessionEncoder;
|
||||||
|
private final ProtocolEngine<CreatorSession> creatorEngine;
|
||||||
|
private final ProtocolEngine<InviteeSession> inviteeEngine;
|
||||||
|
private final ProtocolEngine<PeerSession> peerEngine;
|
||||||
private final Group localGroup;
|
private final Group localGroup;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
protected GroupInvitationManagerImpl(DatabaseComponent db,
|
protected GroupInvitationManagerImpl(DatabaseComponent db,
|
||||||
ClientHelper clientHelper, MetadataParser metadataParser,
|
ClientHelper clientHelper, MetadataParser metadataParser,
|
||||||
ContactGroupFactory contactGroupFactory) {
|
ContactGroupFactory contactGroupFactory,
|
||||||
|
PrivateGroupFactory privateGroupFactory,
|
||||||
|
PrivateGroupManager privateGroupManager,
|
||||||
|
MessageParser messageParser, SessionParser sessionParser,
|
||||||
|
SessionEncoder sessionEncoder,
|
||||||
|
ProtocolEngineFactory engineFactory) {
|
||||||
super(db, clientHelper, metadataParser);
|
super(db, clientHelper, metadataParser);
|
||||||
this.contactGroupFactory = contactGroupFactory;
|
this.contactGroupFactory = contactGroupFactory;
|
||||||
|
this.privateGroupFactory = privateGroupFactory;
|
||||||
|
this.privateGroupManager = privateGroupManager;
|
||||||
|
this.messageParser = messageParser;
|
||||||
|
this.sessionParser = sessionParser;
|
||||||
|
this.sessionEncoder = sessionEncoder;
|
||||||
|
creatorEngine = engineFactory.createCreatorEngine();
|
||||||
|
inviteeEngine = engineFactory.createInviteeEngine();
|
||||||
|
peerEngine = engineFactory.createPeerEngine();
|
||||||
localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID);
|
localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,26 +102,31 @@ public class GroupInvitationManagerImpl extends ConversationClientImpl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addingContact(Transaction txn, Contact c) throws DbException {
|
public void addingContact(Transaction txn, Contact c) throws DbException {
|
||||||
|
// Create a group to share with the contact
|
||||||
|
Group g = getContactGroup(c);
|
||||||
|
// Return if we've already set things up for this contact
|
||||||
|
if (db.containsGroup(txn, g.getId())) return;
|
||||||
|
// Store the group and share it with the contact
|
||||||
|
db.addGroup(txn, g);
|
||||||
|
db.setVisibleToContact(txn, c.getId(), g.getId(), true);
|
||||||
|
// Attach the contact ID to the group
|
||||||
|
BdfDictionary meta = new BdfDictionary();
|
||||||
|
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
|
||||||
try {
|
try {
|
||||||
// Create a group to share with the contact
|
|
||||||
Group g = getContactGroup(c);
|
|
||||||
// Return if we've already set things up for this contact
|
|
||||||
if (db.containsGroup(txn, g.getId())) return;
|
|
||||||
// Store the group and share it with the contact
|
|
||||||
db.addGroup(txn, g);
|
|
||||||
db.setVisibleToContact(txn, c.getId(), g.getId(), true);
|
|
||||||
// Attach the contact ID to the group
|
|
||||||
BdfDictionary meta = new BdfDictionary();
|
|
||||||
meta.put(CONTACT_ID, c.getId().getInt());
|
|
||||||
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
|
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
|
||||||
} catch (FormatException e) {
|
} catch (FormatException e) {
|
||||||
throw new DbException(e);
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
// If the contact belongs to any private groups, create a peer session
|
||||||
|
for (Group pg : db.getGroups(txn, PrivateGroupManager.CLIENT_ID)) {
|
||||||
|
if (privateGroupManager.isMember(txn, pg.getId(), c.getAuthor()))
|
||||||
|
addingMember(txn, pg.getId(), c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removingContact(Transaction txn, Contact c) throws DbException {
|
public void removingContact(Transaction txn, Contact c) throws DbException {
|
||||||
// remove the contact group (all messages will be removed with it)
|
// Remove the contact group (all messages will be removed with it)
|
||||||
db.removeGroup(txn, getContactGroup(c));
|
db.removeGroup(txn, getContactGroup(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,43 +137,416 @@ public class GroupInvitationManagerImpl extends ConversationClientImpl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
|
protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
|
||||||
BdfDictionary meta) throws DbException, FormatException {
|
BdfDictionary bdfMeta) throws DbException, FormatException {
|
||||||
|
// Parse the metadata
|
||||||
|
MessageMetadata meta = messageParser.parseMetadata(bdfMeta);
|
||||||
|
// Look up the session, if there is one
|
||||||
|
SessionId sessionId = getSessionId(meta.getPrivateGroupId());
|
||||||
|
StoredSession ss = getSession(txn, m.getGroupId(), sessionId);
|
||||||
|
// Handle the message
|
||||||
|
Session session;
|
||||||
|
MessageId storageId;
|
||||||
|
if (ss == null) {
|
||||||
|
session = handleFirstMessage(txn, m, body, meta);
|
||||||
|
storageId = createStorageId(txn, m.getGroupId());
|
||||||
|
} else {
|
||||||
|
session = handleMessage(txn, m, body, meta, ss.bdfSession);
|
||||||
|
storageId = ss.storageId;
|
||||||
|
}
|
||||||
|
// Store the updated session
|
||||||
|
storeSession(txn, storageId, session);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private SessionId getSessionId(GroupId privateGroupId) {
|
||||||
public void sendInvitation(GroupId groupId, ContactId contactId,
|
return new SessionId(privateGroupId.getBytes());
|
||||||
String message) throws DbException {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Nullable
|
||||||
public void respondToInvitation(PrivateGroup g, Contact c, boolean accept)
|
private StoredSession getSession(Transaction txn, GroupId contactGroupId,
|
||||||
|
SessionId sessionId) throws DbException, FormatException {
|
||||||
|
BdfDictionary query = sessionParser.getSessionQuery(sessionId);
|
||||||
|
Map<MessageId, BdfDictionary> results = clientHelper
|
||||||
|
.getMessageMetadataAsDictionary(txn, contactGroupId, query);
|
||||||
|
if (results.size() > 1) throw new DbException();
|
||||||
|
if (results.isEmpty()) return null;
|
||||||
|
return new StoredSession(results.keySet().iterator().next(),
|
||||||
|
results.values().iterator().next());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Session handleFirstMessage(Transaction txn, Message m, BdfList body,
|
||||||
|
MessageMetadata meta) throws DbException, FormatException {
|
||||||
|
GroupId privateGroupId = meta.getPrivateGroupId();
|
||||||
|
MessageType type = meta.getMessageType();
|
||||||
|
if (type == INVITE) {
|
||||||
|
InviteeSession session =
|
||||||
|
new InviteeSession(m.getGroupId(), privateGroupId);
|
||||||
|
return handleMessage(txn, m, body, type, session, inviteeEngine);
|
||||||
|
} else if (type == JOIN) {
|
||||||
|
PeerSession session =
|
||||||
|
new PeerSession(m.getGroupId(), privateGroupId);
|
||||||
|
return handleMessage(txn, m, body, type, session, peerEngine);
|
||||||
|
} else {
|
||||||
|
throw new FormatException(); // Invalid first message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Session handleMessage(Transaction txn, Message m, BdfList body,
|
||||||
|
MessageMetadata meta, BdfDictionary bdfSession)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
MessageType type = meta.getMessageType();
|
||||||
|
Role role = sessionParser.getRole(bdfSession);
|
||||||
|
if (role == CREATOR) {
|
||||||
|
CreatorSession session = sessionParser
|
||||||
|
.parseCreatorSession(m.getGroupId(), bdfSession);
|
||||||
|
return handleMessage(txn, m, body, type, session, creatorEngine);
|
||||||
|
} else if (role == INVITEE) {
|
||||||
|
InviteeSession session = sessionParser
|
||||||
|
.parseInviteeSession(m.getGroupId(), bdfSession);
|
||||||
|
return handleMessage(txn, m, body, type, session, inviteeEngine);
|
||||||
|
} else if (role == PEER) {
|
||||||
|
PeerSession session = sessionParser
|
||||||
|
.parsePeerSession(m.getGroupId(), bdfSession);
|
||||||
|
return handleMessage(txn, m, body, type, session, peerEngine);
|
||||||
|
} else {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <S extends Session> S handleMessage(Transaction txn, Message m,
|
||||||
|
BdfList body, MessageType type, S session, ProtocolEngine<S> engine)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
if (type == INVITE) {
|
||||||
|
InviteMessage invite = messageParser.parseInviteMessage(m, body);
|
||||||
|
return engine.onInviteMessage(txn, session, invite);
|
||||||
|
} else if (type == JOIN) {
|
||||||
|
JoinMessage join = messageParser.parseJoinMessage(m, body);
|
||||||
|
return engine.onJoinMessage(txn, session, join);
|
||||||
|
} else if (type == LEAVE) {
|
||||||
|
LeaveMessage leave = messageParser.parseLeaveMessage(m, body);
|
||||||
|
return engine.onLeaveMessage(txn, session, leave);
|
||||||
|
} else if (type == ABORT) {
|
||||||
|
AbortMessage abort = messageParser.parseAbortMessage(m, body);
|
||||||
|
return engine.onAbortMessage(txn, session, abort);
|
||||||
|
} else {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MessageId createStorageId(Transaction txn, GroupId g)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
|
Message m = clientHelper.createMessageForStoringMetadata(g);
|
||||||
|
db.addLocalMessage(txn, m, new Metadata(), false);
|
||||||
|
return m.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void storeSession(Transaction txn, MessageId storageId,
|
||||||
|
Session session) throws DbException, FormatException {
|
||||||
|
BdfDictionary d = sessionEncoder.encodeSession(session);
|
||||||
|
clientHelper.mergeMessageMetadata(txn, storageId, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void respondToInvitation(SessionId id, boolean accept)
|
public void sendInvitation(GroupId privateGroupId, ContactId c,
|
||||||
|
@Nullable String message, long timestamp, byte[] signature)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
|
SessionId sessionId = getSessionId(privateGroupId);
|
||||||
|
Transaction txn = db.startTransaction(false);
|
||||||
|
try {
|
||||||
|
// Look up the session, if there is one
|
||||||
|
Contact contact = db.getContact(txn, c);
|
||||||
|
GroupId contactGroupId = getContactGroup(contact).getId();
|
||||||
|
StoredSession ss = getSession(txn, contactGroupId, sessionId);
|
||||||
|
// Create or parse the session
|
||||||
|
CreatorSession session;
|
||||||
|
MessageId storageId;
|
||||||
|
if (ss == null) {
|
||||||
|
// This is the first invite - create a new session
|
||||||
|
session = new CreatorSession(contactGroupId, privateGroupId);
|
||||||
|
storageId = createStorageId(txn, contactGroupId);
|
||||||
|
} else {
|
||||||
|
// An earlier invite was declined, so we already have a session
|
||||||
|
session = sessionParser
|
||||||
|
.parseCreatorSession(contactGroupId, ss.bdfSession);
|
||||||
|
storageId = ss.storageId;
|
||||||
|
}
|
||||||
|
// Handle the invite action
|
||||||
|
session = creatorEngine.onInviteAction(txn, session, message,
|
||||||
|
timestamp, signature);
|
||||||
|
// Store the updated session
|
||||||
|
storeSession(txn, storageId, session);
|
||||||
|
db.commitTransaction(txn);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e);
|
||||||
|
} finally {
|
||||||
|
db.endTransaction(txn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<InvitationMessage> getInvitationMessages(
|
public void respondToInvitation(ContactId c, PrivateGroup g, boolean accept)
|
||||||
ContactId contactId) throws DbException {
|
throws DbException {
|
||||||
Collection<InvitationMessage> invitations =
|
respondToInvitation(c, getSessionId(g.getId()), accept);
|
||||||
new ArrayList<InvitationMessage>();
|
}
|
||||||
|
|
||||||
return invitations;
|
@Override
|
||||||
|
public void respondToInvitation(ContactId c, SessionId sessionId,
|
||||||
|
boolean accept) throws DbException {
|
||||||
|
Transaction txn = db.startTransaction(false);
|
||||||
|
try {
|
||||||
|
// Look up the session
|
||||||
|
Contact contact = db.getContact(txn, c);
|
||||||
|
GroupId contactGroupId = getContactGroup(contact).getId();
|
||||||
|
StoredSession ss = getSession(txn, contactGroupId, sessionId);
|
||||||
|
if (ss == null) throw new IllegalArgumentException();
|
||||||
|
// Parse the session
|
||||||
|
InviteeSession session = sessionParser
|
||||||
|
.parseInviteeSession(contactGroupId, ss.bdfSession);
|
||||||
|
// Handle the join or leave action
|
||||||
|
if (accept) session = inviteeEngine.onJoinAction(txn, session);
|
||||||
|
else session = inviteeEngine.onLeaveAction(txn, session);
|
||||||
|
// Store the updated session
|
||||||
|
storeSession(txn, ss.storageId, session);
|
||||||
|
db.commitTransaction(txn);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e);
|
||||||
|
} finally {
|
||||||
|
db.endTransaction(txn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <S extends Session> S handleAction(Transaction txn,
|
||||||
|
LocalAction type, S session, ProtocolEngine<S> engine)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
if (type == LocalAction.INVITE) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
} else if (type == LocalAction.JOIN) {
|
||||||
|
return engine.onJoinAction(txn, session);
|
||||||
|
} else if (type == LocalAction.LEAVE) {
|
||||||
|
return engine.onLeaveAction(txn, session);
|
||||||
|
} else if (type == LocalAction.MEMBER_ADDED) {
|
||||||
|
return engine.onMemberAddedAction(txn, session);
|
||||||
|
} else {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<InvitationMessage> getInvitationMessages(ContactId c)
|
||||||
|
throws DbException {
|
||||||
|
List<InvitationMessage> messages;
|
||||||
|
Transaction txn = db.startTransaction(true);
|
||||||
|
try {
|
||||||
|
Contact contact = db.getContact(txn, c);
|
||||||
|
GroupId contactGroupId = getContactGroup(contact).getId();
|
||||||
|
BdfDictionary query = messageParser.getMessagesVisibleInUiQuery();
|
||||||
|
Map<MessageId, BdfDictionary> results = clientHelper
|
||||||
|
.getMessageMetadataAsDictionary(txn, contactGroupId, query);
|
||||||
|
messages = new ArrayList<InvitationMessage>(results.size());
|
||||||
|
for (Entry<MessageId, BdfDictionary> e : results.entrySet()) {
|
||||||
|
MessageId m = e.getKey();
|
||||||
|
MessageMetadata meta =
|
||||||
|
messageParser.parseMetadata(e.getValue());
|
||||||
|
MessageStatus status = db.getMessageStatus(txn, c, m);
|
||||||
|
MessageType type = meta.getMessageType();
|
||||||
|
if (type == INVITE) {
|
||||||
|
messages.add(parseInvitationRequest(txn, c, contactGroupId,
|
||||||
|
m, meta, status));
|
||||||
|
} else if (type == JOIN) {
|
||||||
|
messages.add(parseInvitationResponse(c, contactGroupId, m,
|
||||||
|
meta, status, true));
|
||||||
|
} else if (type == LEAVE) {
|
||||||
|
messages.add(parseInvitationResponse(c, contactGroupId, m,
|
||||||
|
meta, status, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.commitTransaction(txn);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e);
|
||||||
|
} finally {
|
||||||
|
db.endTransaction(txn);
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupInvitationRequest parseInvitationRequest(Transaction txn,
|
||||||
|
ContactId c, GroupId contactGroupId, MessageId m,
|
||||||
|
MessageMetadata meta, MessageStatus status)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
SessionId sessionId = getSessionId(meta.getPrivateGroupId());
|
||||||
|
// Look up the invite message to get the details of the private group
|
||||||
|
InviteMessage invite = getInviteMessage(txn, m);
|
||||||
|
return new GroupInvitationRequest(m, sessionId, contactGroupId, c,
|
||||||
|
invite.getMessage(), invite.getGroupName(), invite.getCreator(),
|
||||||
|
meta.isAvailableToAnswer(), meta.getTimestamp(), meta.isLocal(),
|
||||||
|
status.isSent(), status.isSeen(), meta.isRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
private InviteMessage getInviteMessage(Transaction txn, MessageId m)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
Message message = clientHelper.getMessage(txn, m);
|
||||||
|
if (message == null) throw new DbException();
|
||||||
|
BdfList body = clientHelper.toList(message);
|
||||||
|
return messageParser.parseInviteMessage(message, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupInvitationResponse parseInvitationResponse(ContactId c,
|
||||||
|
GroupId contactGroupId, MessageId m, MessageMetadata meta,
|
||||||
|
MessageStatus status, boolean accept)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
SessionId sessionId = getSessionId(meta.getPrivateGroupId());
|
||||||
|
return new GroupInvitationResponse(m, sessionId, contactGroupId, c,
|
||||||
|
accept, meta.getTimestamp(), meta.isLocal(), status.isSent(),
|
||||||
|
status.isSeen(), meta.isRead());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<GroupInvitationItem> getInvitations() throws DbException {
|
public Collection<GroupInvitationItem> getInvitations() throws DbException {
|
||||||
Collection<GroupInvitationItem> invitations =
|
List<GroupInvitationItem> items = new ArrayList<GroupInvitationItem>();
|
||||||
new ArrayList<GroupInvitationItem>();
|
BdfDictionary query = messageParser.getInvitesAvailableToAnswerQuery();
|
||||||
|
Transaction txn = db.startTransaction(true);
|
||||||
return invitations;
|
try {
|
||||||
|
// Look up the available invite messages for each contact
|
||||||
|
for (Contact c : db.getContacts(txn)) {
|
||||||
|
GroupId contactGroupId = getContactGroup(c).getId();
|
||||||
|
Map<MessageId, BdfDictionary> results =
|
||||||
|
clientHelper.getMessageMetadataAsDictionary(txn,
|
||||||
|
contactGroupId, query);
|
||||||
|
for (MessageId m : results.keySet())
|
||||||
|
items.add(parseGroupInvitationItem(txn, c, m));
|
||||||
|
}
|
||||||
|
db.commitTransaction(txn);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e);
|
||||||
|
} finally {
|
||||||
|
db.endTransaction(txn);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInvitationAllowed(Contact c, GroupId privateGroupId)
|
||||||
|
throws DbException {
|
||||||
|
GroupId contactGroupId = getContactGroup(c).getId();
|
||||||
|
SessionId sessionId = getSessionId(privateGroupId);
|
||||||
|
Transaction txn = db.startTransaction(true);
|
||||||
|
try {
|
||||||
|
StoredSession ss = getSession(txn, contactGroupId, sessionId);
|
||||||
|
db.commitTransaction(txn);
|
||||||
|
// If there's no session, the contact can be invited
|
||||||
|
if (ss == null) return true;
|
||||||
|
// If there's a session, it should be a creator session
|
||||||
|
if (sessionParser.getRole(ss.bdfSession) != CREATOR)
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
// If the session's in the start state, the contact can be invited
|
||||||
|
CreatorSession session = sessionParser
|
||||||
|
.parseCreatorSession(contactGroupId, ss.bdfSession);
|
||||||
|
return session.getState() == START;
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e);
|
||||||
|
} finally {
|
||||||
|
db.endTransaction(txn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupInvitationItem parseGroupInvitationItem(Transaction txn,
|
||||||
|
Contact c, MessageId m) throws DbException, FormatException {
|
||||||
|
InviteMessage invite = getInviteMessage(txn, m);
|
||||||
|
PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
|
||||||
|
invite.getGroupName(), invite.getCreator(), invite.getSalt());
|
||||||
|
return new GroupInvitationItem(privateGroup, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addingMember(Transaction txn, GroupId privateGroupId, Author a)
|
||||||
|
throws DbException {
|
||||||
|
// If the member is a contact, handle the add member action
|
||||||
|
for (Contact c : db.getContactsByAuthorId(txn, a.getId()))
|
||||||
|
addingMember(txn, privateGroupId, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addingMember(Transaction txn, GroupId privateGroupId,
|
||||||
|
Contact c) throws DbException {
|
||||||
|
try {
|
||||||
|
// Look up the session for the contact, if there is one
|
||||||
|
GroupId contactGroupId = getContactGroup(c).getId();
|
||||||
|
SessionId sessionId = getSessionId(privateGroupId);
|
||||||
|
StoredSession ss = getSession(txn, contactGroupId, sessionId);
|
||||||
|
// Create or parse the session
|
||||||
|
Session session;
|
||||||
|
MessageId storageId;
|
||||||
|
if (ss == null) {
|
||||||
|
// If there's no session the contact must be a peer,
|
||||||
|
// otherwise we would have exchanged invitation messages
|
||||||
|
PeerSession peerSession =
|
||||||
|
new PeerSession(contactGroupId, privateGroupId);
|
||||||
|
// Handle the action
|
||||||
|
session = peerEngine.onMemberAddedAction(txn, peerSession);
|
||||||
|
storageId = createStorageId(txn, contactGroupId);
|
||||||
|
} else {
|
||||||
|
// Handle the action
|
||||||
|
session = handleAction(txn, LocalAction.MEMBER_ADDED,
|
||||||
|
contactGroupId, ss.bdfSession);
|
||||||
|
storageId = ss.storageId;
|
||||||
|
}
|
||||||
|
// Store the updated session
|
||||||
|
storeSession(txn, storageId, session);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removingGroup(Transaction txn, GroupId privateGroupId)
|
||||||
|
throws DbException {
|
||||||
|
SessionId sessionId = getSessionId(privateGroupId);
|
||||||
|
// If we have any sessions in progress, tell the contacts we're leaving
|
||||||
|
try {
|
||||||
|
for (Contact c : db.getContacts(txn)) {
|
||||||
|
// Look up the session for the contact, if there is one
|
||||||
|
GroupId contactGroupId = getContactGroup(c).getId();
|
||||||
|
StoredSession ss = getSession(txn, contactGroupId, sessionId);
|
||||||
|
if (ss == null) continue; // No session for this contact
|
||||||
|
// Handle the action
|
||||||
|
Session session = handleAction(txn, LocalAction.LEAVE,
|
||||||
|
contactGroupId, ss.bdfSession);
|
||||||
|
// Store the updated session
|
||||||
|
storeSession(txn, ss.storageId, session);
|
||||||
|
}
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Session handleAction(Transaction txn, LocalAction a,
|
||||||
|
GroupId contactGroupId, BdfDictionary bdfSession)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
Role role = sessionParser.getRole(bdfSession);
|
||||||
|
if (role == CREATOR) {
|
||||||
|
CreatorSession session = sessionParser
|
||||||
|
.parseCreatorSession(contactGroupId, bdfSession);
|
||||||
|
return handleAction(txn, a, session, creatorEngine);
|
||||||
|
} else if (role == INVITEE) {
|
||||||
|
InviteeSession session = sessionParser
|
||||||
|
.parseInviteeSession(contactGroupId, bdfSession);
|
||||||
|
return handleAction(txn, a, session, inviteeEngine);
|
||||||
|
} else if (role == PEER) {
|
||||||
|
PeerSession session = sessionParser
|
||||||
|
.parsePeerSession(contactGroupId, bdfSession);
|
||||||
|
return handleAction(txn, a, session, peerEngine);
|
||||||
|
} else {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class StoredSession {
|
||||||
|
|
||||||
|
private final MessageId storageId;
|
||||||
|
private final BdfDictionary bdfSession;
|
||||||
|
|
||||||
|
private StoredSession(MessageId storageId, BdfDictionary bdfSession) {
|
||||||
|
this.storageId = storageId;
|
||||||
|
this.bdfSession = bdfSession;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
abstract class GroupInvitationMessage {
|
||||||
|
|
||||||
|
private final MessageId id;
|
||||||
|
private final GroupId contactGroupId, privateGroupId;
|
||||||
|
private final long timestamp;
|
||||||
|
|
||||||
|
GroupInvitationMessage(MessageId id, GroupId contactGroupId,
|
||||||
|
GroupId privateGroupId, long timestamp) {
|
||||||
|
this.id = id;
|
||||||
|
this.contactGroupId = contactGroupId;
|
||||||
|
this.privateGroupId = privateGroupId;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageId getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupId getContactGroupId() {
|
||||||
|
return contactGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupId getPrivateGroupId() {
|
||||||
|
return privateGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
|
import org.briarproject.api.contact.ContactManager;
|
||||||
|
import org.briarproject.api.data.MetadataEncoder;
|
||||||
|
import org.briarproject.api.identity.AuthorFactory;
|
||||||
|
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||||
|
import org.briarproject.api.messaging.ConversationManager;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
|
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||||
|
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||||
|
import org.briarproject.api.sync.ValidationManager;
|
||||||
|
import org.briarproject.api.system.Clock;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
|
||||||
|
import static org.briarproject.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
public class GroupInvitationModule {
|
||||||
|
|
||||||
|
public static class EagerSingletons {
|
||||||
|
@Inject
|
||||||
|
GroupInvitationValidator groupInvitationValidator;
|
||||||
|
@Inject
|
||||||
|
GroupInvitationManager groupInvitationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
GroupInvitationManager provideGroupInvitationManager(
|
||||||
|
GroupInvitationManagerImpl groupInvitationManager,
|
||||||
|
LifecycleManager lifecycleManager,
|
||||||
|
ValidationManager validationManager, ContactManager contactManager,
|
||||||
|
PrivateGroupManager privateGroupManager,
|
||||||
|
ConversationManager conversationManager) {
|
||||||
|
lifecycleManager.registerClient(groupInvitationManager);
|
||||||
|
validationManager.registerIncomingMessageHook(CLIENT_ID,
|
||||||
|
groupInvitationManager);
|
||||||
|
contactManager.registerAddContactHook(groupInvitationManager);
|
||||||
|
contactManager.registerRemoveContactHook(groupInvitationManager);
|
||||||
|
privateGroupManager.registerPrivateGroupHook(groupInvitationManager);
|
||||||
|
conversationManager.registerConversationClient(groupInvitationManager);
|
||||||
|
return groupInvitationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
GroupInvitationValidator provideGroupInvitationValidator(
|
||||||
|
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||||
|
Clock clock, AuthorFactory authorFactory,
|
||||||
|
PrivateGroupFactory privateGroupFactory,
|
||||||
|
MessageEncoder messageEncoder,
|
||||||
|
ValidationManager validationManager) {
|
||||||
|
GroupInvitationValidator validator = new GroupInvitationValidator(
|
||||||
|
clientHelper, metadataEncoder, clock, authorFactory,
|
||||||
|
privateGroupFactory, messageEncoder);
|
||||||
|
validationManager.registerMessageValidator(CLIENT_ID, validator);
|
||||||
|
return validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
GroupInvitationFactory provideGroupInvitationFactory(
|
||||||
|
GroupInvitationFactoryImpl groupInvitationFactory) {
|
||||||
|
return groupInvitationFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
MessageParser provideMessageParser(MessageParserImpl messageParser) {
|
||||||
|
return messageParser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
MessageEncoder provideMessageEncoder(MessageEncoderImpl messageEncoder) {
|
||||||
|
return messageEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
SessionParser provideSessionParser(SessionParserImpl sessionParser) {
|
||||||
|
return sessionParser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
SessionEncoder provideSessionEncoder(SessionEncoderImpl sessionEncoder) {
|
||||||
|
return sessionEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
ProtocolEngineFactory provideProtocolEngineFactory(
|
||||||
|
ProtocolEngineFactoryImpl protocolEngineFactory) {
|
||||||
|
return protocolEngineFactory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
import org.briarproject.api.UniqueId;
|
||||||
|
import org.briarproject.api.clients.BdfMessageContext;
|
||||||
|
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.identity.Author;
|
||||||
|
import org.briarproject.api.identity.AuthorFactory;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroup;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
|
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.system.Clock;
|
||||||
|
import org.briarproject.clients.BdfMessageValidator;
|
||||||
|
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||||
|
import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||||
|
import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
|
||||||
|
import static org.briarproject.api.privategroup.PrivateGroupConstants.GROUP_SALT_LENGTH;
|
||||||
|
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_INVITATION_MSG_LENGTH;
|
||||||
|
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.ABORT;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.INVITE;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.JOIN;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.LEAVE;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class GroupInvitationValidator extends BdfMessageValidator {
|
||||||
|
|
||||||
|
private final AuthorFactory authorFactory;
|
||||||
|
private final PrivateGroupFactory privateGroupFactory;
|
||||||
|
private final MessageEncoder messageEncoder;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
GroupInvitationValidator(ClientHelper clientHelper,
|
||||||
|
MetadataEncoder metadataEncoder, Clock clock,
|
||||||
|
AuthorFactory authorFactory,
|
||||||
|
PrivateGroupFactory privateGroupFactory,
|
||||||
|
MessageEncoder messageEncoder) {
|
||||||
|
super(clientHelper, metadataEncoder, clock);
|
||||||
|
this.authorFactory = authorFactory;
|
||||||
|
this.privateGroupFactory = privateGroupFactory;
|
||||||
|
this.messageEncoder = messageEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BdfMessageContext validateMessage(Message m, Group g,
|
||||||
|
BdfList body) throws FormatException {
|
||||||
|
MessageType type = MessageType.fromValue(body.getLong(0).intValue());
|
||||||
|
switch (type) {
|
||||||
|
case INVITE:
|
||||||
|
return validateInviteMessage(m, body);
|
||||||
|
case JOIN:
|
||||||
|
return validateJoinMessage(m, body);
|
||||||
|
case LEAVE:
|
||||||
|
return validateLeaveMessage(m, body);
|
||||||
|
case ABORT:
|
||||||
|
return validateAbortMessage(m, body);
|
||||||
|
default:
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BdfMessageContext validateInviteMessage(Message m, BdfList body)
|
||||||
|
throws FormatException {
|
||||||
|
checkSize(body, 7);
|
||||||
|
String groupName = body.getString(1);
|
||||||
|
checkLength(groupName, 1, MAX_GROUP_NAME_LENGTH);
|
||||||
|
String creatorName = body.getString(2);
|
||||||
|
checkLength(creatorName, 1, MAX_AUTHOR_NAME_LENGTH);
|
||||||
|
byte[] creatorPublicKey = body.getRaw(3);
|
||||||
|
checkLength(creatorPublicKey, 1, MAX_PUBLIC_KEY_LENGTH);
|
||||||
|
byte[] salt = body.getRaw(4);
|
||||||
|
checkLength(salt, GROUP_SALT_LENGTH);
|
||||||
|
String message = body.getOptionalString(5);
|
||||||
|
checkLength(message, 1, MAX_GROUP_INVITATION_MSG_LENGTH);
|
||||||
|
byte[] signature = body.getRaw(6);
|
||||||
|
checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
|
||||||
|
// Create the private group
|
||||||
|
Author creator = authorFactory.createAuthor(creatorName,
|
||||||
|
creatorPublicKey);
|
||||||
|
PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
|
||||||
|
groupName, creator, salt);
|
||||||
|
// Verify the signature
|
||||||
|
BdfList signed = BdfList.of(
|
||||||
|
INVITE.getValue(),
|
||||||
|
m.getTimestamp(),
|
||||||
|
m.getGroupId(),
|
||||||
|
privateGroup.getId()
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
clientHelper.verifySignature(signature, creatorPublicKey, signed);
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
// Create the metadata
|
||||||
|
BdfDictionary meta = messageEncoder.encodeMetadata(INVITE,
|
||||||
|
privateGroup.getId(), m.getTimestamp(), false, false, false,
|
||||||
|
false);
|
||||||
|
return new BdfMessageContext(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BdfMessageContext validateJoinMessage(Message m, BdfList body)
|
||||||
|
throws FormatException {
|
||||||
|
checkSize(body, 3);
|
||||||
|
byte[] privateGroupId = body.getRaw(1);
|
||||||
|
checkLength(privateGroupId, UniqueId.LENGTH);
|
||||||
|
byte[] previousMessageId = body.getOptionalRaw(2);
|
||||||
|
checkLength(previousMessageId, UniqueId.LENGTH);
|
||||||
|
BdfDictionary meta = messageEncoder.encodeMetadata(JOIN,
|
||||||
|
new GroupId(privateGroupId), m.getTimestamp(), false, false,
|
||||||
|
false, false);
|
||||||
|
if (previousMessageId == null) {
|
||||||
|
return new BdfMessageContext(meta);
|
||||||
|
} else {
|
||||||
|
MessageId dependency = new MessageId(previousMessageId);
|
||||||
|
return new BdfMessageContext(meta,
|
||||||
|
Collections.singletonList(dependency));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BdfMessageContext validateLeaveMessage(Message m, BdfList body)
|
||||||
|
throws FormatException {
|
||||||
|
checkSize(body, 3);
|
||||||
|
byte[] privateGroupId = body.getRaw(1);
|
||||||
|
checkLength(privateGroupId, UniqueId.LENGTH);
|
||||||
|
byte[] previousMessageId = body.getOptionalRaw(2);
|
||||||
|
checkLength(previousMessageId, UniqueId.LENGTH);
|
||||||
|
BdfDictionary meta = messageEncoder.encodeMetadata(LEAVE,
|
||||||
|
new GroupId(privateGroupId), m.getTimestamp(), false, false,
|
||||||
|
false, false);
|
||||||
|
if (previousMessageId == null) {
|
||||||
|
return new BdfMessageContext(meta);
|
||||||
|
} else {
|
||||||
|
MessageId dependency = new MessageId(previousMessageId);
|
||||||
|
return new BdfMessageContext(meta,
|
||||||
|
Collections.singletonList(dependency));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BdfMessageContext validateAbortMessage(Message m, BdfList body)
|
||||||
|
throws FormatException {
|
||||||
|
checkSize(body, 2);
|
||||||
|
byte[] privateGroupId = body.getRaw(1);
|
||||||
|
checkLength(privateGroupId, UniqueId.LENGTH);
|
||||||
|
BdfDictionary meta = messageEncoder.encodeMetadata(ABORT,
|
||||||
|
new GroupId(privateGroupId), m.getTimestamp(), false, false,
|
||||||
|
false, false);
|
||||||
|
return new BdfMessageContext(meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class InviteAction {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final String message;
|
||||||
|
private final long timestamp;
|
||||||
|
private final byte[] signature;
|
||||||
|
|
||||||
|
InviteAction(@Nullable String message, long timestamp, byte[] signature) {
|
||||||
|
this.message = message;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] getSignature() {
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.identity.Author;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class InviteMessage extends GroupInvitationMessage {
|
||||||
|
|
||||||
|
private final String groupName;
|
||||||
|
private final Author creator;
|
||||||
|
private final byte[] salt, signature;
|
||||||
|
@Nullable
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
InviteMessage(MessageId id, GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
long timestamp, String groupName, Author creator, byte[] salt,
|
||||||
|
@Nullable String message, byte[] signature) {
|
||||||
|
super(id, contactGroupId, privateGroupId, timestamp);
|
||||||
|
this.groupName = groupName;
|
||||||
|
this.creator = creator;
|
||||||
|
this.salt = salt;
|
||||||
|
this.message = message;
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getGroupName() {
|
||||||
|
return groupName;
|
||||||
|
}
|
||||||
|
|
||||||
|
Author getCreator() {
|
||||||
|
return creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] getSalt() {
|
||||||
|
return salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] getSignature() {
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
|
import org.briarproject.api.clients.ProtocolStateException;
|
||||||
|
import org.briarproject.api.contact.ContactId;
|
||||||
|
import org.briarproject.api.db.DatabaseComponent;
|
||||||
|
import org.briarproject.api.db.DbException;
|
||||||
|
import org.briarproject.api.db.Transaction;
|
||||||
|
import org.briarproject.api.identity.Author;
|
||||||
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
|
import org.briarproject.api.sync.Message;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
import org.briarproject.api.system.Clock;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
import static org.briarproject.privategroup.invitation.InviteeState.DISSOLVED;
|
||||||
|
import static org.briarproject.privategroup.invitation.InviteeState.ERROR;
|
||||||
|
import static org.briarproject.privategroup.invitation.InviteeState.INVITED;
|
||||||
|
import static org.briarproject.privategroup.invitation.InviteeState.INVITEE_JOINED;
|
||||||
|
import static org.briarproject.privategroup.invitation.InviteeState.INVITEE_LEFT;
|
||||||
|
import static org.briarproject.privategroup.invitation.InviteeState.START;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
|
||||||
|
|
||||||
|
InviteeProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
|
||||||
|
PrivateGroupManager privateGroupManager,
|
||||||
|
PrivateGroupFactory privateGroupFactory,
|
||||||
|
GroupMessageFactory groupMessageFactory,
|
||||||
|
IdentityManager identityManager, MessageParser messageParser,
|
||||||
|
MessageEncoder messageEncoder, Clock clock) {
|
||||||
|
super(db, clientHelper, privateGroupManager, privateGroupFactory,
|
||||||
|
groupMessageFactory, identityManager, messageParser,
|
||||||
|
messageEncoder, clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InviteeSession onInviteAction(Transaction txn, InviteeSession s,
|
||||||
|
@Nullable String message, long timestamp, byte[] signature)
|
||||||
|
throws DbException {
|
||||||
|
throw new UnsupportedOperationException(); // Invalid in this role
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InviteeSession onJoinAction(Transaction txn, InviteeSession s)
|
||||||
|
throws DbException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
case INVITEE_JOINED:
|
||||||
|
case INVITEE_LEFT:
|
||||||
|
case DISSOLVED:
|
||||||
|
case ERROR:
|
||||||
|
throw new ProtocolStateException(); // Invalid in these states
|
||||||
|
case INVITED:
|
||||||
|
return onLocalAccept(txn, s);
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InviteeSession onLeaveAction(Transaction txn, InviteeSession s)
|
||||||
|
throws DbException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
case INVITEE_LEFT:
|
||||||
|
case DISSOLVED:
|
||||||
|
case ERROR:
|
||||||
|
return s; // Ignored in these states
|
||||||
|
case INVITED:
|
||||||
|
return onLocalDecline(txn, s);
|
||||||
|
case INVITEE_JOINED:
|
||||||
|
return onLocalLeave(txn, s);
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InviteeSession onMemberAddedAction(Transaction txn, InviteeSession s)
|
||||||
|
throws DbException {
|
||||||
|
return s; // Ignored in this role
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InviteeSession onInviteMessage(Transaction txn, InviteeSession s,
|
||||||
|
InviteMessage m) throws DbException, FormatException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
return onRemoteInvite(txn, s, m);
|
||||||
|
case INVITED:
|
||||||
|
case INVITEE_JOINED:
|
||||||
|
case INVITEE_LEFT:
|
||||||
|
case DISSOLVED:
|
||||||
|
return abort(txn, s); // Invalid in these states
|
||||||
|
case ERROR:
|
||||||
|
return s; // Ignored in this state
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InviteeSession onJoinMessage(Transaction txn, InviteeSession s,
|
||||||
|
JoinMessage m) throws DbException, FormatException {
|
||||||
|
return abort(txn, s); // Invalid in this role
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InviteeSession onLeaveMessage(Transaction txn, InviteeSession s,
|
||||||
|
LeaveMessage m) throws DbException, FormatException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
case DISSOLVED:
|
||||||
|
return abort(txn, s); // Invalid in these states
|
||||||
|
case INVITED:
|
||||||
|
case INVITEE_JOINED:
|
||||||
|
case INVITEE_LEFT:
|
||||||
|
return onRemoteLeave(txn, s, m);
|
||||||
|
case ERROR:
|
||||||
|
return s; // Ignored in this state
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InviteeSession onAbortMessage(Transaction txn, InviteeSession s,
|
||||||
|
AbortMessage m) throws DbException, FormatException {
|
||||||
|
return abort(txn, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InviteeSession onLocalAccept(Transaction txn, InviteeSession s)
|
||||||
|
throws DbException {
|
||||||
|
// Mark the invite message unavailable to answer
|
||||||
|
MessageId inviteId = s.getLastRemoteMessageId();
|
||||||
|
if (inviteId == null) throw new IllegalStateException();
|
||||||
|
markMessageAvailableToAnswer(txn, inviteId, false);
|
||||||
|
// Send a JOIN message
|
||||||
|
Message sent = sendJoinMessage(txn, s, true);
|
||||||
|
try {
|
||||||
|
// Subscribe to the private group
|
||||||
|
subscribeToPrivateGroup(txn, inviteId);
|
||||||
|
// Start syncing the private group with the contact
|
||||||
|
syncPrivateGroupWithContact(txn, s, true);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e); // Invalid group metadata
|
||||||
|
}
|
||||||
|
// Move to the INVITEE_JOINED state
|
||||||
|
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||||
|
s.getInviteTimestamp(), INVITEE_JOINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InviteeSession onLocalDecline(Transaction txn, InviteeSession s)
|
||||||
|
throws DbException {
|
||||||
|
// Mark the invite message unavailable to answer
|
||||||
|
MessageId inviteId = s.getLastRemoteMessageId();
|
||||||
|
if (inviteId == null) throw new IllegalStateException();
|
||||||
|
markMessageAvailableToAnswer(txn, inviteId, false);
|
||||||
|
// Send a LEAVE message
|
||||||
|
Message sent = sendLeaveMessage(txn, s, true);
|
||||||
|
// Move to the START state
|
||||||
|
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||||
|
s.getInviteTimestamp(), START);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InviteeSession onLocalLeave(Transaction txn, InviteeSession s)
|
||||||
|
throws DbException {
|
||||||
|
// Send a LEAVE message
|
||||||
|
Message sent = sendLeaveMessage(txn, s, false);
|
||||||
|
// Move to the INVITEE_LEFT state
|
||||||
|
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||||
|
s.getInviteTimestamp(), INVITEE_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InviteeSession onRemoteInvite(Transaction txn, InviteeSession s,
|
||||||
|
InviteMessage m) throws DbException, FormatException {
|
||||||
|
// The timestamp must be higher than the last invite message, if any
|
||||||
|
if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
|
||||||
|
// Check that the contact is the creator
|
||||||
|
ContactId contactId = getContactId(txn, s.getContactGroupId());
|
||||||
|
Author contact = db.getContact(txn, contactId).getAuthor();
|
||||||
|
if (!contact.getId().equals(m.getCreator().getId()))
|
||||||
|
return abort(txn, s);
|
||||||
|
// Mark the invite message visible in the UI and available to answer
|
||||||
|
markMessageVisibleInUi(txn, m.getId(), true);
|
||||||
|
markMessageAvailableToAnswer(txn, m.getId(), true);
|
||||||
|
// Move to the INVITED state
|
||||||
|
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||||
|
m.getTimestamp(), INVITED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InviteeSession onRemoteLeave(Transaction txn, InviteeSession s,
|
||||||
|
LeaveMessage m) throws DbException, FormatException {
|
||||||
|
// The timestamp must be higher than the last invite message, if any
|
||||||
|
if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
|
||||||
|
// The dependency, if any, must be the last remote message
|
||||||
|
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||||
|
return abort(txn, s);
|
||||||
|
try {
|
||||||
|
// Stop syncing the private group with the contact
|
||||||
|
syncPrivateGroupWithContact(txn, s, false);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e); // Invalid group metadata
|
||||||
|
}
|
||||||
|
// Mark the group dissolved
|
||||||
|
privateGroupManager.markGroupDissolved(txn, s.getPrivateGroupId());
|
||||||
|
// Move to the DISSOLVED state
|
||||||
|
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||||
|
s.getInviteTimestamp(), DISSOLVED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InviteeSession abort(Transaction txn, InviteeSession s)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
// If the session has already been aborted, do nothing
|
||||||
|
if (s.getState() == ERROR) return s;
|
||||||
|
// Mark any invite messages in the session unavailable to answer
|
||||||
|
markInvitesUnavailableToAnswer(txn, s);
|
||||||
|
// Stop syncing the private group with the contact, if we subscribe
|
||||||
|
if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId()))
|
||||||
|
syncPrivateGroupWithContact(txn, s, false);
|
||||||
|
// Send an ABORT message
|
||||||
|
Message sent = sendAbortMessage(txn, s);
|
||||||
|
// Move to the ERROR state
|
||||||
|
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||||
|
s.getInviteTimestamp(), ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
import static org.briarproject.privategroup.invitation.InviteeState.START;
|
||||||
|
import static org.briarproject.privategroup.invitation.Role.INVITEE;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class InviteeSession extends Session<InviteeState> {
|
||||||
|
|
||||||
|
private final InviteeState state;
|
||||||
|
|
||||||
|
InviteeSession(GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
@Nullable MessageId lastLocalMessageId,
|
||||||
|
@Nullable MessageId lastRemoteMessageId, long localTimestamp,
|
||||||
|
long inviteTimestamp, InviteeState state) {
|
||||||
|
super(contactGroupId, privateGroupId, lastLocalMessageId,
|
||||||
|
lastRemoteMessageId, localTimestamp, inviteTimestamp);
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
InviteeSession(GroupId contactGroupId, GroupId privateGroupId) {
|
||||||
|
this(contactGroupId, privateGroupId, null, null, 0, 0, START);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Role getRole() {
|
||||||
|
return INVITEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
InviteeState getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
|
||||||
|
enum InviteeState implements State {
|
||||||
|
|
||||||
|
START(0), INVITED(1), INVITEE_JOINED(2), INVITEE_LEFT(3), DISSOLVED(4),
|
||||||
|
ERROR(5);
|
||||||
|
|
||||||
|
private final int value;
|
||||||
|
|
||||||
|
InviteeState(int value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static InviteeState fromValue(int value) throws FormatException {
|
||||||
|
for (InviteeState s : values()) if (s.value == value) return s;
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class JoinMessage extends GroupInvitationMessage {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final MessageId previousMessageId;
|
||||||
|
|
||||||
|
JoinMessage(MessageId id, GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
long timestamp, @Nullable MessageId previousMessageId) {
|
||||||
|
super(id, contactGroupId, privateGroupId, timestamp);
|
||||||
|
this.previousMessageId = previousMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
MessageId getPreviousMessageId() {
|
||||||
|
return previousMessageId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class LeaveMessage extends GroupInvitationMessage {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final MessageId previousMessageId;
|
||||||
|
|
||||||
|
LeaveMessage(MessageId id, GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
long timestamp, @Nullable MessageId previousMessageId) {
|
||||||
|
super(id, contactGroupId, privateGroupId, timestamp);
|
||||||
|
this.previousMessageId = previousMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
MessageId getPreviousMessageId() {
|
||||||
|
return previousMessageId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
enum LocalAction {
|
||||||
|
|
||||||
|
INVITE, JOIN, LEAVE, MEMBER_ADDED
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
|
import org.briarproject.api.identity.Author;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.Message;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
interface MessageEncoder {
|
||||||
|
|
||||||
|
BdfDictionary encodeMetadata(MessageType type, GroupId privateGroupId,
|
||||||
|
long timestamp, boolean local, boolean read, boolean visible,
|
||||||
|
boolean available);
|
||||||
|
|
||||||
|
void setVisibleInUi(BdfDictionary meta, boolean visible);
|
||||||
|
|
||||||
|
void setAvailableToAnswer(BdfDictionary meta, boolean available);
|
||||||
|
|
||||||
|
Message encodeInviteMessage(GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
long timestamp, String groupName, Author creator, byte[] salt,
|
||||||
|
@Nullable String message, byte[] signature);
|
||||||
|
|
||||||
|
Message encodeJoinMessage(GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
long timestamp, @Nullable MessageId previousMessageId);
|
||||||
|
|
||||||
|
Message encodeLeaveMessage(GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
long timestamp, @Nullable MessageId previousMessageId);
|
||||||
|
|
||||||
|
Message encodeAbortMessage(GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
long timestamp);
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
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.identity.Author;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.Message;
|
||||||
|
import org.briarproject.api.sync.MessageFactory;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_LOCAL;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_MESSAGE_TYPE;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_PRIVATE_GROUP_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_TIMESTAMP;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_VISIBLE_IN_UI;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.ABORT;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.INVITE;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.JOIN;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.LEAVE;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class MessageEncoderImpl implements MessageEncoder {
|
||||||
|
|
||||||
|
private final ClientHelper clientHelper;
|
||||||
|
private final MessageFactory messageFactory;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
MessageEncoderImpl(ClientHelper clientHelper,
|
||||||
|
MessageFactory messageFactory) {
|
||||||
|
this.clientHelper = clientHelper;
|
||||||
|
this.messageFactory = messageFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BdfDictionary encodeMetadata(MessageType type,
|
||||||
|
GroupId privateGroupId, long timestamp, boolean local, boolean read,
|
||||||
|
boolean visible, boolean available) {
|
||||||
|
BdfDictionary meta = new BdfDictionary();
|
||||||
|
meta.put(MSG_KEY_MESSAGE_TYPE, type.getValue());
|
||||||
|
meta.put(MSG_KEY_PRIVATE_GROUP_ID, privateGroupId);
|
||||||
|
meta.put(MSG_KEY_TIMESTAMP, timestamp);
|
||||||
|
meta.put(MSG_KEY_LOCAL, local);
|
||||||
|
meta.put(MSG_KEY_READ, read);
|
||||||
|
meta.put(MSG_KEY_VISIBLE_IN_UI, visible);
|
||||||
|
meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available);
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setVisibleInUi(BdfDictionary meta, boolean visible) {
|
||||||
|
meta.put(MSG_KEY_VISIBLE_IN_UI, visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAvailableToAnswer(BdfDictionary meta, boolean available) {
|
||||||
|
meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message encodeInviteMessage(GroupId contactGroupId,
|
||||||
|
GroupId privateGroupId, long timestamp, String groupName,
|
||||||
|
Author creator, byte[] salt, @Nullable String message,
|
||||||
|
byte[] signature) {
|
||||||
|
BdfList body = BdfList.of(
|
||||||
|
INVITE.getValue(),
|
||||||
|
groupName,
|
||||||
|
creator.getName(),
|
||||||
|
creator.getPublicKey(),
|
||||||
|
salt,
|
||||||
|
message,
|
||||||
|
signature
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
return messageFactory.createMessage(contactGroupId, timestamp,
|
||||||
|
clientHelper.toByteArray(body));
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message encodeJoinMessage(GroupId contactGroupId,
|
||||||
|
GroupId privateGroupId, long timestamp,
|
||||||
|
@Nullable MessageId previousMessageId) {
|
||||||
|
BdfList body = BdfList.of(
|
||||||
|
JOIN.getValue(),
|
||||||
|
privateGroupId,
|
||||||
|
previousMessageId
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
return messageFactory.createMessage(contactGroupId, timestamp,
|
||||||
|
clientHelper.toByteArray(body));
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message encodeLeaveMessage(GroupId contactGroupId,
|
||||||
|
GroupId privateGroupId, long timestamp,
|
||||||
|
@Nullable MessageId previousMessageId) {
|
||||||
|
BdfList body = BdfList.of(
|
||||||
|
LEAVE.getValue(),
|
||||||
|
privateGroupId,
|
||||||
|
previousMessageId
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
return messageFactory.createMessage(contactGroupId, timestamp,
|
||||||
|
clientHelper.toByteArray(body));
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message encodeAbortMessage(GroupId contactGroupId,
|
||||||
|
GroupId privateGroupId, long timestamp) {
|
||||||
|
BdfList body = BdfList.of(
|
||||||
|
ABORT.getValue(),
|
||||||
|
privateGroupId
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
return messageFactory.createMessage(contactGroupId, timestamp,
|
||||||
|
clientHelper.toByteArray(body));
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
|
||||||
|
class MessageMetadata {
|
||||||
|
|
||||||
|
private final MessageType type;
|
||||||
|
private final GroupId privateGroupId;
|
||||||
|
private final long timestamp;
|
||||||
|
private final boolean local, read, visible, available;
|
||||||
|
|
||||||
|
MessageMetadata(MessageType type, GroupId privateGroupId,
|
||||||
|
long timestamp, boolean local, boolean read, boolean visible,
|
||||||
|
boolean available) {
|
||||||
|
this.privateGroupId = privateGroupId;
|
||||||
|
this.type = type;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.local = local;
|
||||||
|
this.read = read;
|
||||||
|
this.visible = visible;
|
||||||
|
this.available = available;
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageType getMessageType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupId getPrivateGroupId() {
|
||||||
|
return privateGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isLocal() {
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isRead() {
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isVisibleInConversation() {
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isAvailableToAnswer() {
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
|
import org.briarproject.api.data.BdfList;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.Message;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
interface MessageParser {
|
||||||
|
|
||||||
|
BdfDictionary getMessagesVisibleInUiQuery();
|
||||||
|
|
||||||
|
BdfDictionary getInvitesAvailableToAnswerQuery();
|
||||||
|
|
||||||
|
BdfDictionary getInvitesAvailableToAnswerQuery(GroupId privateGroupId);
|
||||||
|
|
||||||
|
MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException;
|
||||||
|
|
||||||
|
InviteMessage parseInviteMessage(Message m, BdfList body)
|
||||||
|
throws FormatException;
|
||||||
|
|
||||||
|
JoinMessage parseJoinMessage(Message m, BdfList body)
|
||||||
|
throws FormatException;
|
||||||
|
|
||||||
|
LeaveMessage parseLeaveMessage(Message m, BdfList body)
|
||||||
|
throws FormatException;
|
||||||
|
|
||||||
|
AbortMessage parseAbortMessage(Message m, BdfList body)
|
||||||
|
throws FormatException;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
|
import org.briarproject.api.data.BdfEntry;
|
||||||
|
import org.briarproject.api.data.BdfList;
|
||||||
|
import org.briarproject.api.identity.Author;
|
||||||
|
import org.briarproject.api.identity.AuthorFactory;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroup;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.Message;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_LOCAL;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_MESSAGE_TYPE;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_PRIVATE_GROUP_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_TIMESTAMP;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_VISIBLE_IN_UI;
|
||||||
|
import static org.briarproject.privategroup.invitation.MessageType.INVITE;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class MessageParserImpl implements MessageParser {
|
||||||
|
|
||||||
|
private final AuthorFactory authorFactory;
|
||||||
|
private final PrivateGroupFactory privateGroupFactory;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
MessageParserImpl(AuthorFactory authorFactory,
|
||||||
|
PrivateGroupFactory privateGroupFactory) {
|
||||||
|
this.authorFactory = authorFactory;
|
||||||
|
this.privateGroupFactory = privateGroupFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BdfDictionary getMessagesVisibleInUiQuery() {
|
||||||
|
return BdfDictionary.of(new BdfEntry(MSG_KEY_VISIBLE_IN_UI, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BdfDictionary getInvitesAvailableToAnswerQuery() {
|
||||||
|
return BdfDictionary.of(
|
||||||
|
new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true),
|
||||||
|
new BdfEntry(MSG_KEY_MESSAGE_TYPE, INVITE.getValue())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BdfDictionary getInvitesAvailableToAnswerQuery(
|
||||||
|
GroupId privateGroupId) {
|
||||||
|
return BdfDictionary.of(
|
||||||
|
new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true),
|
||||||
|
new BdfEntry(MSG_KEY_MESSAGE_TYPE, INVITE.getValue()),
|
||||||
|
new BdfEntry(MSG_KEY_PRIVATE_GROUP_ID, privateGroupId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MessageMetadata parseMetadata(BdfDictionary meta)
|
||||||
|
throws FormatException {
|
||||||
|
MessageType type = MessageType.fromValue(
|
||||||
|
meta.getLong(MSG_KEY_MESSAGE_TYPE).intValue());
|
||||||
|
GroupId privateGroupId =
|
||||||
|
new GroupId(meta.getRaw(MSG_KEY_PRIVATE_GROUP_ID));
|
||||||
|
long timestamp = meta.getLong(MSG_KEY_TIMESTAMP);
|
||||||
|
boolean local = meta.getBoolean(MSG_KEY_LOCAL);
|
||||||
|
boolean read = meta.getBoolean(MSG_KEY_READ, false);
|
||||||
|
boolean visible = meta.getBoolean(MSG_KEY_VISIBLE_IN_UI, false);
|
||||||
|
boolean available = meta.getBoolean(MSG_KEY_AVAILABLE_TO_ANSWER, false);
|
||||||
|
return new MessageMetadata(type, privateGroupId, timestamp, local, read,
|
||||||
|
visible, available);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InviteMessage parseInviteMessage(Message m, BdfList body)
|
||||||
|
throws FormatException {
|
||||||
|
String groupName = body.getString(1);
|
||||||
|
String creatorName = body.getString(2);
|
||||||
|
byte[] creatorPublicKey = body.getRaw(3);
|
||||||
|
byte[] salt = body.getRaw(4);
|
||||||
|
String message = body.getOptionalString(5);
|
||||||
|
byte[] signature = body.getRaw(6);
|
||||||
|
Author creator = authorFactory.createAuthor(creatorName,
|
||||||
|
creatorPublicKey);
|
||||||
|
PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
|
||||||
|
groupName, creator, salt);
|
||||||
|
return new InviteMessage(m.getId(), m.getGroupId(),
|
||||||
|
privateGroup.getId(), m.getTimestamp(), groupName, creator,
|
||||||
|
salt, message, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JoinMessage parseJoinMessage(Message m, BdfList body)
|
||||||
|
throws FormatException {
|
||||||
|
GroupId privateGroupId = new GroupId(body.getRaw(1));
|
||||||
|
byte[] b = body.getOptionalRaw(2);
|
||||||
|
MessageId previousMessageId = b == null ? null : new MessageId(b);
|
||||||
|
return new JoinMessage(m.getId(), m.getGroupId(), privateGroupId,
|
||||||
|
m.getTimestamp(), previousMessageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LeaveMessage parseLeaveMessage(Message m, BdfList body)
|
||||||
|
throws FormatException {
|
||||||
|
GroupId privateGroupId = new GroupId(body.getRaw(1));
|
||||||
|
byte[] b = body.getOptionalRaw(2);
|
||||||
|
MessageId previousMessageId = b == null ? null : new MessageId(b);
|
||||||
|
return new LeaveMessage(m.getId(), m.getGroupId(), privateGroupId,
|
||||||
|
m.getTimestamp(), previousMessageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbortMessage parseAbortMessage(Message m, BdfList body)
|
||||||
|
throws FormatException {
|
||||||
|
GroupId privateGroupId = new GroupId(body.getRaw(1));
|
||||||
|
return new AbortMessage(m.getId(), m.getGroupId(), privateGroupId,
|
||||||
|
m.getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
|
||||||
|
enum MessageType {
|
||||||
|
|
||||||
|
INVITE(0), JOIN(1), LEAVE(2), ABORT(3);
|
||||||
|
|
||||||
|
private final int value;
|
||||||
|
|
||||||
|
MessageType(int value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MessageType fromValue(int value) throws FormatException {
|
||||||
|
for (MessageType m : values()) if (m.value == value) return m;
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
|
import org.briarproject.api.clients.ProtocolStateException;
|
||||||
|
import org.briarproject.api.db.DatabaseComponent;
|
||||||
|
import org.briarproject.api.db.DbException;
|
||||||
|
import org.briarproject.api.db.Transaction;
|
||||||
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
|
import org.briarproject.api.sync.Message;
|
||||||
|
import org.briarproject.api.system.Clock;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
import static org.briarproject.privategroup.invitation.PeerState.AWAIT_MEMBER;
|
||||||
|
import static org.briarproject.privategroup.invitation.PeerState.BOTH_JOINED;
|
||||||
|
import static org.briarproject.privategroup.invitation.PeerState.ERROR;
|
||||||
|
import static org.briarproject.privategroup.invitation.PeerState.LOCAL_JOINED;
|
||||||
|
import static org.briarproject.privategroup.invitation.PeerState.NEITHER_JOINED;
|
||||||
|
import static org.briarproject.privategroup.invitation.PeerState.REMOTE_JOINED;
|
||||||
|
import static org.briarproject.privategroup.invitation.PeerState.START;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
|
||||||
|
|
||||||
|
PeerProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
|
||||||
|
PrivateGroupManager privateGroupManager,
|
||||||
|
PrivateGroupFactory privateGroupFactory,
|
||||||
|
GroupMessageFactory groupMessageFactory,
|
||||||
|
IdentityManager identityManager, MessageParser messageParser,
|
||||||
|
MessageEncoder messageEncoder, Clock clock) {
|
||||||
|
super(db, clientHelper, privateGroupManager, privateGroupFactory,
|
||||||
|
groupMessageFactory, identityManager, messageParser,
|
||||||
|
messageEncoder, clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerSession onInviteAction(Transaction txn, PeerSession s,
|
||||||
|
@Nullable String message, long timestamp, byte[] signature)
|
||||||
|
throws DbException {
|
||||||
|
throw new UnsupportedOperationException(); // Invalid in this role
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerSession onJoinAction(Transaction txn, PeerSession s)
|
||||||
|
throws DbException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
case AWAIT_MEMBER:
|
||||||
|
case LOCAL_JOINED:
|
||||||
|
case BOTH_JOINED:
|
||||||
|
case ERROR:
|
||||||
|
throw new ProtocolStateException(); // Invalid in these states
|
||||||
|
case NEITHER_JOINED:
|
||||||
|
return onLocalJoinFirst(txn, s);
|
||||||
|
case REMOTE_JOINED:
|
||||||
|
return onLocalJoinSecond(txn, s);
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerSession onLeaveAction(Transaction txn, PeerSession s)
|
||||||
|
throws DbException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
case AWAIT_MEMBER:
|
||||||
|
case NEITHER_JOINED:
|
||||||
|
case REMOTE_JOINED:
|
||||||
|
case ERROR:
|
||||||
|
return s; // Ignored in these states
|
||||||
|
case LOCAL_JOINED:
|
||||||
|
return onLocalLeaveSecond(txn, s);
|
||||||
|
case BOTH_JOINED:
|
||||||
|
return onLocalLeaveFirst(txn, s);
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerSession onMemberAddedAction(Transaction txn, PeerSession s)
|
||||||
|
throws DbException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
case AWAIT_MEMBER:
|
||||||
|
return onRemoteMemberAdded(s);
|
||||||
|
case NEITHER_JOINED:
|
||||||
|
case LOCAL_JOINED:
|
||||||
|
case REMOTE_JOINED:
|
||||||
|
case BOTH_JOINED:
|
||||||
|
case ERROR:
|
||||||
|
throw new ProtocolStateException(); // Invalid in these states
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerSession onInviteMessage(Transaction txn, PeerSession s,
|
||||||
|
InviteMessage m) throws DbException, FormatException {
|
||||||
|
return abort(txn, s); // Invalid in this role
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerSession onJoinMessage(Transaction txn, PeerSession s,
|
||||||
|
JoinMessage m) throws DbException, FormatException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case AWAIT_MEMBER:
|
||||||
|
case REMOTE_JOINED:
|
||||||
|
case BOTH_JOINED:
|
||||||
|
return abort(txn, s); // Invalid in these states
|
||||||
|
case START:
|
||||||
|
case NEITHER_JOINED:
|
||||||
|
return onRemoteJoinFirst(txn, s, m);
|
||||||
|
case LOCAL_JOINED:
|
||||||
|
return onRemoteJoinSecond(txn, s, m);
|
||||||
|
case ERROR:
|
||||||
|
return s; // Ignored in this state
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerSession onLeaveMessage(Transaction txn, PeerSession s,
|
||||||
|
LeaveMessage m) throws DbException, FormatException {
|
||||||
|
switch (s.getState()) {
|
||||||
|
case START:
|
||||||
|
case NEITHER_JOINED:
|
||||||
|
case LOCAL_JOINED:
|
||||||
|
return abort(txn, s);
|
||||||
|
case AWAIT_MEMBER:
|
||||||
|
case REMOTE_JOINED:
|
||||||
|
return onRemoteLeaveSecond(txn, s, m);
|
||||||
|
case BOTH_JOINED:
|
||||||
|
return onRemoteLeaveFirst(txn, s, m);
|
||||||
|
case ERROR:
|
||||||
|
return s; // Ignored in this state
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerSession onAbortMessage(Transaction txn, PeerSession s,
|
||||||
|
AbortMessage m) throws DbException, FormatException {
|
||||||
|
return abort(txn, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PeerSession onLocalJoinFirst(Transaction txn, PeerSession s)
|
||||||
|
throws DbException {
|
||||||
|
// Send a JOIN message
|
||||||
|
Message sent = sendJoinMessage(txn, s, false);
|
||||||
|
// Move to the LOCAL_JOINED state
|
||||||
|
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||||
|
LOCAL_JOINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PeerSession onLocalJoinSecond(Transaction txn, PeerSession s)
|
||||||
|
throws DbException {
|
||||||
|
// Send a JOIN message
|
||||||
|
Message sent = sendJoinMessage(txn, s, false);
|
||||||
|
try {
|
||||||
|
// Start syncing the private group with the contact
|
||||||
|
syncPrivateGroupWithContact(txn, s, true);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e); // Invalid group metadata
|
||||||
|
}
|
||||||
|
// Move to the BOTH_JOINED state
|
||||||
|
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||||
|
BOTH_JOINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PeerSession onLocalLeaveFirst(Transaction txn, PeerSession s)
|
||||||
|
throws DbException {
|
||||||
|
// Send a LEAVE message
|
||||||
|
Message sent = sendLeaveMessage(txn, s, false);
|
||||||
|
try {
|
||||||
|
// Stop syncing the private group with the contact
|
||||||
|
syncPrivateGroupWithContact(txn, s, false);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e); // Invalid group metadata
|
||||||
|
}
|
||||||
|
// Move to the REMOTE_JOINED state
|
||||||
|
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||||
|
REMOTE_JOINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PeerSession onLocalLeaveSecond(Transaction txn, PeerSession s)
|
||||||
|
throws DbException {
|
||||||
|
// Send a LEAVE message
|
||||||
|
Message sent = sendLeaveMessage(txn, s, false);
|
||||||
|
// Move to the NEITHER_JOINED state
|
||||||
|
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||||
|
NEITHER_JOINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PeerSession onRemoteMemberAdded(PeerSession s) {
|
||||||
|
// Move to the NEITHER_JOINED or REMOTE_JOINED state
|
||||||
|
PeerState next = s.getState() == START ? NEITHER_JOINED : REMOTE_JOINED;
|
||||||
|
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
s.getLastLocalMessageId(), s.getLastRemoteMessageId(),
|
||||||
|
s.getLocalTimestamp(), next);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PeerSession onRemoteJoinFirst(Transaction txn, PeerSession s,
|
||||||
|
JoinMessage m) throws DbException, FormatException {
|
||||||
|
// The dependency, if any, must be the last remote message
|
||||||
|
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||||
|
return abort(txn, s);
|
||||||
|
// Move to the AWAIT_MEMBER or REMOTE_JOINED state
|
||||||
|
PeerState next = s.getState() == START ? AWAIT_MEMBER : REMOTE_JOINED;
|
||||||
|
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||||
|
next);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PeerSession onRemoteJoinSecond(Transaction txn, PeerSession s,
|
||||||
|
JoinMessage m) throws DbException, FormatException {
|
||||||
|
// The dependency, if any, must be the last remote message
|
||||||
|
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||||
|
return abort(txn, s);
|
||||||
|
// Start syncing the private group with the contact
|
||||||
|
syncPrivateGroupWithContact(txn, s, true);
|
||||||
|
// Move to the BOTH_JOINED state
|
||||||
|
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||||
|
BOTH_JOINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PeerSession onRemoteLeaveSecond(Transaction txn, PeerSession s,
|
||||||
|
LeaveMessage m) throws DbException, FormatException {
|
||||||
|
// The dependency, if any, must be the last remote message
|
||||||
|
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||||
|
return abort(txn, s);
|
||||||
|
// Move to the START or NEITHER_JOINED state
|
||||||
|
PeerState next = s.getState() == AWAIT_MEMBER ? START : NEITHER_JOINED;
|
||||||
|
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||||
|
next);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PeerSession onRemoteLeaveFirst(Transaction txn, PeerSession s,
|
||||||
|
LeaveMessage m) throws DbException, FormatException {
|
||||||
|
// The dependency, if any, must be the last remote message
|
||||||
|
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||||
|
return abort(txn, s);
|
||||||
|
// Stop syncing the private group with the contact
|
||||||
|
syncPrivateGroupWithContact(txn, s, false);
|
||||||
|
// Move to the LOCAL_JOINED state
|
||||||
|
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||||
|
LOCAL_JOINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PeerSession abort(Transaction txn, PeerSession s)
|
||||||
|
throws DbException, FormatException {
|
||||||
|
// If the session has already been aborted, do nothing
|
||||||
|
if (s.getState() == ERROR) return s;
|
||||||
|
// Stop syncing the private group with the contact, if we subscribe
|
||||||
|
if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId()))
|
||||||
|
syncPrivateGroupWithContact(txn, s, false);
|
||||||
|
// Send an ABORT message
|
||||||
|
Message sent = sendAbortMessage(txn, s);
|
||||||
|
// Move to the ERROR state
|
||||||
|
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||||
|
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||||
|
ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
import static org.briarproject.privategroup.invitation.PeerState.START;
|
||||||
|
import static org.briarproject.privategroup.invitation.Role.PEER;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class PeerSession extends Session<PeerState> {
|
||||||
|
|
||||||
|
private final PeerState state;
|
||||||
|
|
||||||
|
PeerSession(GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
@Nullable MessageId lastLocalMessageId,
|
||||||
|
@Nullable MessageId lastRemoteMessageId, long localTimestamp,
|
||||||
|
PeerState state) {
|
||||||
|
super(contactGroupId, privateGroupId, lastLocalMessageId,
|
||||||
|
lastRemoteMessageId, localTimestamp, 0);
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
PeerSession(GroupId contactGroupId, GroupId privateGroupId) {
|
||||||
|
this(contactGroupId, privateGroupId, null, null, 0, START);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Role getRole() {
|
||||||
|
return PEER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
PeerState getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
|
||||||
|
enum PeerState implements State {
|
||||||
|
|
||||||
|
START(0), AWAIT_MEMBER(1), NEITHER_JOINED(2), LOCAL_JOINED(3),
|
||||||
|
REMOTE_JOINED(4), BOTH_JOINED(5), ERROR(6);
|
||||||
|
|
||||||
|
private final int value;
|
||||||
|
|
||||||
|
PeerState(int value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PeerState fromValue(int value) throws FormatException {
|
||||||
|
for (PeerState s : values()) if (s.value == value) return s;
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
import org.briarproject.api.db.DbException;
|
||||||
|
import org.briarproject.api.db.Transaction;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
interface ProtocolEngine<S extends Session> {
|
||||||
|
|
||||||
|
S onInviteAction(Transaction txn, S session, @Nullable String message,
|
||||||
|
long timestamp, byte[] signature) throws DbException;
|
||||||
|
|
||||||
|
S onJoinAction(Transaction txn, S session) throws DbException;
|
||||||
|
|
||||||
|
S onLeaveAction(Transaction txn, S session) throws DbException;
|
||||||
|
|
||||||
|
S onMemberAddedAction(Transaction txn, S session) throws DbException;
|
||||||
|
|
||||||
|
S onInviteMessage(Transaction txn, S session, InviteMessage m)
|
||||||
|
throws DbException, FormatException;
|
||||||
|
|
||||||
|
S onJoinMessage(Transaction txn, S session, JoinMessage m)
|
||||||
|
throws DbException, FormatException;
|
||||||
|
|
||||||
|
S onLeaveMessage(Transaction txn, S session, LeaveMessage m)
|
||||||
|
throws DbException, FormatException;
|
||||||
|
|
||||||
|
S onAbortMessage(Transaction txn, S session, AbortMessage m)
|
||||||
|
throws DbException, FormatException;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
interface ProtocolEngineFactory {
|
||||||
|
|
||||||
|
ProtocolEngine<CreatorSession> createCreatorEngine();
|
||||||
|
|
||||||
|
ProtocolEngine<InviteeSession> createInviteeEngine();
|
||||||
|
|
||||||
|
ProtocolEngine<PeerSession> createPeerEngine();
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
|
import org.briarproject.api.db.DatabaseComponent;
|
||||||
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
|
import org.briarproject.api.system.Clock;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class ProtocolEngineFactoryImpl implements ProtocolEngineFactory {
|
||||||
|
|
||||||
|
private final DatabaseComponent db;
|
||||||
|
private final ClientHelper clientHelper;
|
||||||
|
private final PrivateGroupManager privateGroupManager;
|
||||||
|
private final PrivateGroupFactory privateGroupFactory;
|
||||||
|
private final GroupMessageFactory groupMessageFactory;
|
||||||
|
private final IdentityManager identityManager;
|
||||||
|
private final MessageParser messageParser;
|
||||||
|
private final MessageEncoder messageEncoder;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProtocolEngineFactoryImpl(DatabaseComponent db, ClientHelper clientHelper,
|
||||||
|
PrivateGroupManager privateGroupManager,
|
||||||
|
PrivateGroupFactory privateGroupFactory,
|
||||||
|
GroupMessageFactory groupMessageFactory,
|
||||||
|
IdentityManager identityManager, MessageParser messageParser,
|
||||||
|
MessageEncoder messageEncoder,
|
||||||
|
Clock clock) {
|
||||||
|
this.db = db;
|
||||||
|
this.clientHelper = clientHelper;
|
||||||
|
this.privateGroupManager = privateGroupManager;
|
||||||
|
this.privateGroupFactory = privateGroupFactory;
|
||||||
|
this.groupMessageFactory = groupMessageFactory;
|
||||||
|
this.identityManager = identityManager;
|
||||||
|
this.messageParser = messageParser;
|
||||||
|
this.messageEncoder = messageEncoder;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProtocolEngine<CreatorSession> createCreatorEngine() {
|
||||||
|
return new CreatorProtocolEngine(db, clientHelper, privateGroupManager,
|
||||||
|
privateGroupFactory, groupMessageFactory, identityManager,
|
||||||
|
messageParser, messageEncoder, clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProtocolEngine<InviteeSession> createInviteeEngine() {
|
||||||
|
return new InviteeProtocolEngine(db, clientHelper, privateGroupManager,
|
||||||
|
privateGroupFactory, groupMessageFactory, identityManager,
|
||||||
|
messageParser, messageEncoder, clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProtocolEngine<PeerSession> createPeerEngine() {
|
||||||
|
return new PeerProtocolEngine(db, clientHelper, privateGroupManager,
|
||||||
|
privateGroupFactory, groupMessageFactory, identityManager,
|
||||||
|
messageParser, messageEncoder, clock);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
|
||||||
|
CREATOR(0), INVITEE(1), PEER(2);
|
||||||
|
|
||||||
|
private final int value;
|
||||||
|
|
||||||
|
Role(int value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Role fromValue(int value) throws FormatException {
|
||||||
|
for (Role r : values()) if (r.value == value) return r;
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
abstract class Session<S extends State> {
|
||||||
|
|
||||||
|
private final GroupId contactGroupId, privateGroupId;
|
||||||
|
@Nullable
|
||||||
|
private final MessageId lastLocalMessageId, lastRemoteMessageId;
|
||||||
|
private final long localTimestamp, inviteTimestamp;
|
||||||
|
|
||||||
|
Session(GroupId contactGroupId, GroupId privateGroupId,
|
||||||
|
@Nullable MessageId lastLocalMessageId,
|
||||||
|
@Nullable MessageId lastRemoteMessageId, long localTimestamp,
|
||||||
|
long inviteTimestamp) {
|
||||||
|
this.contactGroupId = contactGroupId;
|
||||||
|
this.privateGroupId = privateGroupId;
|
||||||
|
this.lastLocalMessageId = lastLocalMessageId;
|
||||||
|
this.lastRemoteMessageId = lastRemoteMessageId;
|
||||||
|
this.localTimestamp = localTimestamp;
|
||||||
|
this.inviteTimestamp = inviteTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract Role getRole();
|
||||||
|
|
||||||
|
abstract S getState();
|
||||||
|
|
||||||
|
GroupId getContactGroupId() {
|
||||||
|
return contactGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupId getPrivateGroupId() {
|
||||||
|
return privateGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
MessageId getLastLocalMessageId() {
|
||||||
|
return lastLocalMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
MessageId getLastRemoteMessageId() {
|
||||||
|
return lastRemoteMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getLocalTimestamp() {
|
||||||
|
return localTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getInviteTimestamp() {
|
||||||
|
return inviteTimestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
interface SessionEncoder {
|
||||||
|
|
||||||
|
BdfDictionary encodeSession(Session s);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static org.briarproject.api.data.BdfDictionary.NULL_VALUE;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_INVITE_TIMESTAMP;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LOCAL_TIMESTAMP;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_PRIVATE_GROUP_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_ROLE;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_SESSION_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_STATE;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class SessionEncoderImpl implements SessionEncoder {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SessionEncoderImpl() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BdfDictionary encodeSession(Session s) {
|
||||||
|
BdfDictionary d = new BdfDictionary();
|
||||||
|
d.put(SESSION_KEY_SESSION_ID, s.getPrivateGroupId());
|
||||||
|
d.put(SESSION_KEY_PRIVATE_GROUP_ID, s.getPrivateGroupId());
|
||||||
|
MessageId lastLocalMessageId = s.getLastLocalMessageId();
|
||||||
|
if (lastLocalMessageId == null)
|
||||||
|
d.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, NULL_VALUE);
|
||||||
|
else d.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, lastLocalMessageId);
|
||||||
|
MessageId lastRemoteMessageId = s.getLastRemoteMessageId();
|
||||||
|
if (lastRemoteMessageId == null)
|
||||||
|
d.put(SESSION_KEY_LAST_REMOTE_MESSAGE_ID, NULL_VALUE);
|
||||||
|
else d.put(SESSION_KEY_LAST_REMOTE_MESSAGE_ID, lastRemoteMessageId);
|
||||||
|
d.put(SESSION_KEY_LOCAL_TIMESTAMP, s.getLocalTimestamp());
|
||||||
|
d.put(SESSION_KEY_INVITE_TIMESTAMP, s.getInviteTimestamp());
|
||||||
|
d.put(SESSION_KEY_ROLE, s.getRole().getValue());
|
||||||
|
d.put(SESSION_KEY_STATE, s.getState().getValue());
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
import org.briarproject.api.clients.SessionId;
|
||||||
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
interface SessionParser {
|
||||||
|
|
||||||
|
BdfDictionary getSessionQuery(SessionId s);
|
||||||
|
|
||||||
|
Role getRole(BdfDictionary d) throws FormatException;
|
||||||
|
|
||||||
|
CreatorSession parseCreatorSession(GroupId contactGroupId, BdfDictionary d)
|
||||||
|
throws FormatException;
|
||||||
|
|
||||||
|
InviteeSession parseInviteeSession(GroupId contactGroupId, BdfDictionary d)
|
||||||
|
throws FormatException;
|
||||||
|
|
||||||
|
PeerSession parsePeerSession(GroupId contactGroupId, BdfDictionary d)
|
||||||
|
throws FormatException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
|
import org.briarproject.api.clients.SessionId;
|
||||||
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
|
import org.briarproject.api.data.BdfEntry;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_INVITE_TIMESTAMP;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LOCAL_TIMESTAMP;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_PRIVATE_GROUP_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_ROLE;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_SESSION_ID;
|
||||||
|
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_STATE;
|
||||||
|
import static org.briarproject.privategroup.invitation.Role.CREATOR;
|
||||||
|
import static org.briarproject.privategroup.invitation.Role.INVITEE;
|
||||||
|
import static org.briarproject.privategroup.invitation.Role.PEER;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
class SessionParserImpl implements SessionParser {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SessionParserImpl() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BdfDictionary getSessionQuery(SessionId s) {
|
||||||
|
return BdfDictionary.of(new BdfEntry(SESSION_KEY_SESSION_ID, s));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Role getRole(BdfDictionary d) throws FormatException {
|
||||||
|
return Role.fromValue(d.getLong(SESSION_KEY_ROLE).intValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CreatorSession parseCreatorSession(GroupId contactGroupId,
|
||||||
|
BdfDictionary d) throws FormatException {
|
||||||
|
if (getRole(d) != CREATOR) throw new IllegalArgumentException();
|
||||||
|
return new CreatorSession(contactGroupId, getPrivateGroupId(d),
|
||||||
|
getLastLocalMessageId(d), getLastRemoteMessageId(d),
|
||||||
|
getLocalTimestamp(d), getInviteTimestamp(d),
|
||||||
|
CreatorState.fromValue(getState(d)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InviteeSession parseInviteeSession(GroupId contactGroupId,
|
||||||
|
BdfDictionary d) throws FormatException {
|
||||||
|
if (getRole(d) != INVITEE) throw new IllegalArgumentException();
|
||||||
|
return new InviteeSession(contactGroupId, getPrivateGroupId(d),
|
||||||
|
getLastLocalMessageId(d), getLastRemoteMessageId(d),
|
||||||
|
getLocalTimestamp(d), getInviteTimestamp(d),
|
||||||
|
InviteeState.fromValue(getState(d)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerSession parsePeerSession(GroupId contactGroupId,
|
||||||
|
BdfDictionary d) throws FormatException {
|
||||||
|
if (getRole(d) != PEER) throw new IllegalArgumentException();
|
||||||
|
return new PeerSession(contactGroupId, getPrivateGroupId(d),
|
||||||
|
getLastLocalMessageId(d), getLastRemoteMessageId(d),
|
||||||
|
getLocalTimestamp(d), PeerState.fromValue(getState(d)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getState(BdfDictionary d) throws FormatException {
|
||||||
|
return d.getLong(SESSION_KEY_STATE).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupId getPrivateGroupId(BdfDictionary d) throws FormatException {
|
||||||
|
return new GroupId(d.getRaw(SESSION_KEY_PRIVATE_GROUP_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private MessageId getLastLocalMessageId(BdfDictionary d)
|
||||||
|
throws FormatException {
|
||||||
|
byte[] b = d.getOptionalRaw(SESSION_KEY_LAST_LOCAL_MESSAGE_ID);
|
||||||
|
return b == null ? null : new MessageId(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private MessageId getLastRemoteMessageId(BdfDictionary d)
|
||||||
|
throws FormatException {
|
||||||
|
byte[] b = d.getOptionalRaw(SESSION_KEY_LAST_REMOTE_MESSAGE_ID);
|
||||||
|
return b == null ? null : new MessageId(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getLocalTimestamp(BdfDictionary d) throws FormatException {
|
||||||
|
return d.getLong(SESSION_KEY_LOCAL_TIMESTAMP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getInviteTimestamp(BdfDictionary d) throws FormatException {
|
||||||
|
return d.getLong(SESSION_KEY_INVITE_TIMESTAMP);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package org.briarproject.privategroup.invitation;
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
|
||||||
|
int getValue();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user